Multi-Tenant Isolation Enforced by the Database, Not by the Code
Tenant isolation enforced by Postgres RLS, not by application code
The Challenge
- One town hall never sees another: every operational table carries its town hall and FORCE ROW LEVEL SECURITY; requesting another tenant's resource by ID answers 404, never 403
- Tenant inside the transaction: set with SET LOCAL, never a loose SET — the pooler recycles connections and the next request could belong to a different town hall
- Automated gates, not discipline: a new table without an isolation test does not ship, and a schema test walks Postgres and fails the build if it finds a tenant table with RLS off
- Layer boundaries verified: ui → application → domain ← infrastructure, with the domain importing nothing; dependency-cruiser checks it on pre-push and in CI
Solution Architecture
Subdomain Resolution
The town hall is resolved from the Host in the Next.js proxy, before the session. Login runs inside the same tenancy layer as any other table.
Tenancy Layer
All data access goes through withTenant: it opens a transaction, sets the tenant with SET LOCAL and lets the RLS policy do the filtering. There is no direct database connection.
Database
PostgreSQL with Drizzle ORM, chosen for RLS and explicit transaction control. Separate roles for migration and application: neither is a superuser nor can bypass RLS.
Quality
Spec-first (SDD) and strict TDD: unit and isolation tests in Vitest, E2E in Playwright, Biome for lint and formatting, dependency-cruiser for layer boundaries.
Results
RLS
Isolation in the database, not in the code
SDD + TDD
Written spec before the code, test before the implementation
0 BYPASSRLS
No application role can bypass the policy