Layer 01 — API + data
Engine
The backend your product sits on. NestJS on Fastify, Drizzle ORM, roles you configure in JSON, and each customer’s data isolated in its own PostgreSQL schema.
Specification
- Runtime
- NestJS on Fastify, Node.js 20+
- Database
- PostgreSQL, one schema per tenant
- ORM
- Drizzle — thin layer over SQL, no separate query engine
- Queues
- Redis
- Auth
- JWT access + sliding-window refresh tokens
- SSO
- Google, GitHub, Microsoft, Discord, Apple
- Config
- Joi-validated at boot; missing required vars fail the start
- Licence
- MIT
01 — ISOLATION
Isolation the database enforces.
Tenant data is separated at the PostgreSQL schema level, not by a tenant column on every table. Cross-tenant leakage stops being a WHERE clause somebody has to remember and becomes structurally impossible — the other tenant's rows are not in scope.
The cost is real and worth stating plainly: migrations run per schema, and pooling has to be per schema too. Engine handles both, but neither is free.
# request lifecycle
Client request
└→ Fastify / NestJS API
└→ TenantContextService (AsyncLocalStorage)
root request → root schema
tenant request → tenant_<accountId>
Context is resolved once per request and travels in Node's AsyncLocalStorage. Services deep in the stack ask for the current tenant instead of having one threaded through every function signature — so a new service cannot forget to scope itself.
02 — WHAT IT DOES
The parts you would have built anyway.
Tenant provisioning
Creating an account creates its schema. The root schema holds the tenant registry, system settings and global subscriptions; everything operational lives in tenant_<accountId>.
Module-based RBAC
Permissions group by module — view_billing, manage_users — configured in rbac-root.json and rbac-tenant.json. Roles are a config edit, not a migration.
Session policy
auth-policy.json sets concurrent device limits per surface. Exceeding the limit evicts the oldest session rather than refusing the new login.
Per-schema pooling
Each tenant schema gets its own connection pool, with an idle TTL that evicts unused pools before Postgres runs out of connections.
03 — THROUGHPUT
Why Fastify and Drizzle.
Performance is not the reason to pick a template, but it is a reason not to pick the wrong foundation. Swapping the web framework and ORM after you have built on them is the expensive kind of change.
- Express + TypeORM12,000
- Express + Prisma18,000
- Fastify + Drizzle ORM65,000
Requests per second, higher is better. Our own benchmark, from engine/README.md — our hardware, our handlers. Read it as a comparison between these three combinations, not as a prediction of your production numbers. Fastify publishes independent framework-level figures at fastify.dev/benchmarks.
04 — THE ORM
Drizzle wins two of these four. That is the honest picture.
An independent benchmark comparing the four ORMs you would actually weigh up. Read it and you will see no ORM wins everywhere — Prisma is the fastest here on concurrent work and the slowest on sequential, and Sequelize beats Drizzle on concurrent inserts.
Select — concurrent
50k selects via Promise.all()
- Prisma16,216
- Drizzle8,474
- Sequelize8,365
- TypeORM7,687
Select — sequential
50k selects awaited one by one
- Drizzle10,079
- TypeORM7,878
- Sequelize7,607
- Prisma3,493
Insert — one transaction
50k users in a single transaction
- Drizzle6,058
- TypeORM5,962
- Sequelize4,113
- Prisma2,555
Insert — concurrent
50k users via Promise.all()
- Prisma9,563
- Sequelize4,984
- TypeORM4,950
- Drizzle4,066
Average queries per second, higher is better. Source: JS-AK/db-orm-benchmarks — PostgreSQL 16.x, 50,000 queries per run, averaged over 10 runs. Not our benchmark and not our hardware. Measured on drizzle-orm 0.28.6, @prisma/client 5.4.1, sequelize 6.33.0 and typeorm 0.3.17, so it is a snapshot of those versions rather than the current releases.
Why we still chose it
Not peak throughput — consistency. Across these four workloads Drizzle ranges from 4,066 to 10,079 queries per second. Prisma ranges from 2,555 to 16,216. On a template that will run workloads we cannot predict, an ORM with no collapse mode is worth more than one with a higher ceiling and a worse floor.
And the architecture
Drizzle is a thin, typed layer over SQL with no separate query engine to ship, the schema is ordinary TypeScript with no codegen step, and relational reads avoid n+1 by design. Those properties hold regardless of which benchmark you run.
If your workload is dominated by high-concurrency reads, Prisma is the faster choice on this evidence and the template is built so you can swap it. We would rather tell you that than publish the one chart that makes our pick look unbeatable.
05 — START
One command to a running backend.
Setup is idempotent — re-running it skips an existing .env and skips seeding anything already there, so you can run it again after pulling without losing data.
$
→ creates .env with generated JWT secrets
→ runs migrations
→ seeds a root admin + your first tenant company
idempotent — safe to re-run