features
Per-feature folders
Adding product behavior means one folder, not scattered route/service/model edits.
apps/api
The API layer owns security, data, and background work: auth, sessions, OAuth, email, queues, audit log, Stripe billing, structured logging, and an env validator that refuses to boot if anything is missing.
Elysia
typed HTTP surface
Drizzle
SQL-shaped data layer
BullMQ
background work
flowchart LR routes["routes.ts<br/>HTTP · TypeBox · no DB"] service["service.ts<br/>logic + Drizzle<br/>no Elysia"] types["types.ts<br/>shared interfaces"] routes --> service routes -.-> types service -.-> types
Each API feature splits into three files with one job each:
routes.ts owns HTTP and TypeBox validation but never touches the
database; service.ts owns business logic and Drizzle queries but
never imports Elysia; types.ts holds the shared interfaces both
read. Lint rules forbid the cross-imports that would blur the split.
A feature is three files with three jobs. Lint plugins forbid them from leaking into each other: a *.routes.ts that imports drizzle-orm fails the build, and a *.service.ts that imports Elysia’s t does too.
features
Adding product behavior means one folder, not scattered route/service/model edits.
lint
The split survives refactors, new teammates, and agent-written code.
env
process.env is read in one validator. Misconfigured deployments fail before serving traffic.
providers
Email, AI, cache, and queues swap through config; dev runs without vendor keys.
data
TS-first models with SQL-shaped schema and real migration files.
contract
The React app calls a generated client, so server changes become type errors instead of runtime surprises.
A feature folder always looks like (for a hypothetical posts resource):
The shipped auth, users, accounts, billing, dashboard, admin, health, notifications modules are framework. widgets is the only example domain feature, kept as the reference for the account-scoped resource pattern (every read/write filters by accountId). Replace it with your own product domain. Add new resources with bun run new:resource <name>; the scaffolder writes the four-file anatomy and wires it into config/routes.ts so you can’t forget a step.
Short-lived access JWT cookies plus DB-backed refresh sessions.
tenantAccounts are the tenant boundary; users join via memberships with roles.
aclServer-authoritative CASL ability plus plan and feature gates.
billingCheckout, Customer Portal, raw-body webhooks, and DB-backed idempotency.
emailPluggable provider, precompiled templates, queue-aware dispatch.
queuesBullMQ with QueueManager and inline fallback when queues are disabled.
auditFire-and-forget append-only event log for user and system actions.
envTypeBox shape plus hand-written invariants before the API listens.
The architecture is held in place by a family of custom ESLint plugins. bun run validate is the merge gate.
apps/api on GitHub. Start in src/api/ for the feature shape; src/config/ for the boot wiring.
scripts/lint-meta/.