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.

Full setup guide
engine/README.md

→ creates .env with generated JWT secrets

→ runs migrations

→ seeds a root admin + your first tenant company

idempotent — safe to re-run

Next layerUI Frame — the admin dashboard