Backend for Symurr — escalating payment reminders for freelancers, agencies, and consultants.
Node.js + Express + PostgreSQL (via Prisma). This is the API only — it does not include the landing page, which is a separate static HTML file.
| Piece | Status |
|---|---|
| Auth (register/login/refresh/logout) | Fully implemented |
| Invoice CRUD, scoped per user | Fully implemented |
| Escalation logic (5-stage reminder ladder) | Fully implemented |
| Daily cron job that sends reminders | Fully implemented — needs SMTP + Twilio credentials to actually send |
| Stripe Connect onboarding link | Implemented — needs your Stripe account + STRIPE_SECRET_KEY |
| Stripe webhook (marks invoices paid) | Implemented — needs STRIPE_WEBHOOK_SECRET from the Stripe dashboard |
| Reading real invoices from Stripe automatically | Not built yet — right now you create invoices via the API. Auto-importing from a connected Stripe account is the natural next step; ask me when you're ready. |
Nothing here has been tested against a live Stripe/Twilio account — I don't have network access to those services from where I built this. You'll need to test those integrations yourself with test-mode keys first.
# 1. Install dependencies
npm install
# 2. Copy the env template and fill in real values
cp .env.example .env
# 3. Create a Postgres database (locally, or a free one on Supabase/Neon/Railway)
# then put its connection string in DATABASE_URL inside .env
# 4. Generate the Prisma client and run the first migration
npx prisma migrate dev --name init
# 5. Start the dev server (auto-restarts on file changes)
npm run devThe API will be running at http://localhost:4000. Check GET /health to confirm it's up.
- Postgres: easiest free option while learning is Neon or Supabase — both give you a connection string in ~2 minutes.
- Stripe: sign up at stripe.com, grab your test mode secret key from the
Dashboard → Developers → API keys. For the webhook secret, use the Stripe CLI
(
stripe listen --forward-to localhost:4000/api/stripe/webhook) while developing. - Twilio (SMS): sign up at twilio.com, get a trial number + Account SID + Auth Token.
- SMTP (email): for testing, Mailtrap is the easiest — it catches emails in a sandbox inbox instead of really sending them.
Never commit .env — it's already in .gitignore.
There's no separate "how do I let people in" step needed beyond what's built:
POST /api/auth/register creates an account, POST /api/auth/login signs in.
A frontend (the landing page, eventually turned into a full app, or a separate
React dashboard) would call these endpoints and store the returned access token
in memory (not localStorage — see security notes below).
- Passwords: hashed with bcrypt (cost factor 12), never stored or logged in plain text.
- Access tokens: short-lived JWTs (15 min default), sent in the response body, meant to be held in memory on the frontend — not localStorage, which is readable by any injected script (XSS).
- Refresh tokens: long-lived (7 days), but stored as
httpOnly,sameSite=strictcookies — invisible to JavaScript, which blocks both XSS token theft and CSRF. The token itself is never stored in the database in plain form — only its SHA-256 hash — so a leaked database dump can't be replayed as a live session. Refresh tokens rotate on every use: reusing an old one is treated as invalid. - Login errors are generic ("Invalid email or password") on purpose — a different error for "no such account" vs "wrong password" lets attackers enumerate which emails have accounts.
- Rate limiting: a strict limiter (10 requests / 15 min) on auth endpoints specifically, to slow down brute-force and credential-stuffing attempts.
- Every invoice query is scoped to
userId: there is no endpoint where one user can read or modify another user's data by guessing an ID. - Input validation: every request body is validated with Zod before it touches a database query — malformed or unexpected fields are rejected early.
- Stripe webhook signature verification: the webhook route is deliberately wired to receive the raw request body (before Express's JSON parser touches it), because Stripe's signature check requires the exact original bytes. A request that fails this check is rejected outright — this is what stops someone from forging a fake "invoice paid" event.
helmet: sets secure HTTP headers (CSP, no-sniff, no clickjacking, etc.) by default.- Errors never leak internals: the global error handler logs full details server-side but only ever sends a generic message to the client for unexpected (500-level) errors.
What this does not yet include, and you should add before going to
production with real users' money: email verification on signup, 2FA, an
account-lockout policy after repeated failed logins, audit logging, and a
proper secrets manager instead of a .env file.
src/
app.js — Express app setup, middleware, route mounting
server.js — entry point, starts the server + cron scheduler
config/ — Prisma client, Stripe client
middleware/ — auth check, rate limiting, error handling
routes/ — one file per resource (auth, invoices, stripe)
controllers/ — request handlers, one per resource
services/ — reminder stage copy, email sending, SMS sending
jobs/ — the daily cron job that actually sends reminders
utils/ — Zod validators, token helpers
prisma/schema.prisma — database schema (User, Invoice, Client, etc.)