Module architecture
Each feature is organized into three 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/verify— sign 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
sessionsrow; 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_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-versionon launch + hourly, and every request sendsX-App-Versionso the API answers426 Upgrade Requiredfor a stale build. Recovery endpoints (/health,/version,/app-version) are never gated.
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’sADMIN_URLare always allowed.