Skip to main content
The backend half of the kit — Fastify + Drizzle + PostgreSQL + Redis. Single-app: one deployment serves one product.

Module architecture

Each feature is organized into three layers:
Supporting layers: src/config (zod env + constants), src/db (Drizzle client, schema, migrate, seed), src/plugins (cors, rate-limit, auth, swagger, error handler), src/shared (errors, crypto, uuidv7, username rules, email, serializers), src/jobs.
Every service function, handler, schema and type is exported and ready to use — including handlers that no default route mounts yet, so you can wire them into your own routes as you build.

Auth model — passwordless only

One model: a six-digit email code. Sign-in and sign-up are the same act — the first valid code for an unknown address creates the account. No password, no register/login split, no reset, no separate email verification.
  • POST /v1/auth/code — emails a code. 204 for any address (not an enumeration oracle). 10-min life, 5 attempts, single use, hashed at rest, constant-time compare.
  • POST /v1/auth/code/verifysign in · create-and-sign-in (username auto-generated from the email) · deletion_pending (soft-deleted → restoreToken).
  • Google / Apple sign-in available alongside.
  • Access 15 min, refresh 30 days, HS256, rotation on every refresh; each pair bound to a sessions row; live-session check cached in Redis with a DB fallback.
Authentication is passwordless by design — there is intentionally no password, no reset, and no separate email-verification step.
Test / review accounts: TEST_ACCOUNTS=email:6digits,… — a fixed code for App Store / Google Play review and CI. Verifies only against a pre-seeded is_test account, so a fixed code never creates one. Every fixed-code sign-in is logged at warn.

What’s in the box

Interactive reference at /docs (Swagger UI). Other mechanics: username (auto from email, live availability, 30-day change cooldown), email change by code, account deletion that feels instant (sessions killed now) with physical purge after PURGE_AFTER_DAYS.

App version policy & upgrade gate

The app publishes a version policy the client honours — set it in Admin → Settings, no deploy:
  • Latest version → a soft “update available” dot when the installed build is older. A nudge.
  • Min. supported version → a hard gate: a build below it is blocked. Enforced two ways — the app checks /v1/app-version on launch + hourly, and every request sends X-App-Version so the API answers 426 Upgrade Required for a stale build. Recovery endpoints (/health, /version, /app-version) are never gated.
Leave a field empty to disable that half. Separately, API_DEPRECATION / API_SUNSET add the RFC 8594 Deprecation / Sunset headers to every /v1 response when set.

Scripts

Operational notes

  • The container boots migrate → seed → serve.
  • Run purge daily on cron; in the prod image use the compiled path: node dist/jobs/purge.js.
  • Email: point SMTP_* at a transactional provider (Postmark works). Set SPF, DKIM, DMARC — with passwordless auth, a code in spam means nobody can sign in.
  • CORS: CORS_ORIGINS = exact web origin(s); native schemes and the admin’s ADMIN_URL are always allowed.