Capability matrix
Which databases, adapters, and strategies are tested-supported - and which aren't.
TenancyJS only marks a combination supported after a real two-tenant adversarial isolation test on a real database: tenant A and tenant B with colliding ids, and a test that fails if A can ever read B's row. This page is the honest source of truth for what has actually been proven.
This page covers which databases and strategies are supported. For the operations the scoped facade deliberately rejects - raw queries, nested reads/writes, native handles, complex criteria - see Limitations.
By database
Start here - it decides which strategies are even available to you.
| Database | Row-level | Schema-per-tenant | Database-per-tenant | Adapters |
|---|---|---|---|---|
| PostgreSQL | ✅ | ✅ | ✅ | Knex · Lucid · Prisma · TypeORM · Sequelize · Drizzle |
| MySQL | 🧪 | - | ✅ | Prisma · TypeORM · Sequelize · Drizzle · Lucid ³ |
| MongoDB | ✅ ¹ | - | ✅ ² | Mongoose |
| SQL Server | 📋 | 📋 | 📋 | planned - see roadmap |
✅ tested-supported · 🧪 experimental · 📋 planned (not yet available) · - not available
Schema-per-tenant is PostgreSQL-only. MySQL treats SCHEMA and DATABASE as the same namespace,
and MongoDB has no SQL schema/search-path equivalent. Both support the distinct
database-per-tenant strategy through cache-routed resources.
🧪 MySQL row-level is experimental. MySQL has no row-level security, so it's facade-only (no database-level backstop), and it's had less real-world exposure than the Postgres path. It works and is tested, but treat it as experimental until it's proven in production. See how it's enforced.
¹ MongoDB is facade-only. It has no row-level security, so isolation is enforced entirely by the adapter's query facade - using the native model, collection, or connection bypasses it. Treat it as a strong convention, not a database-level guarantee.
² MongoDB database-per-tenant is a routing boundary by default. It becomes database-enforced only when each tenant connection uses credentials restricted to that tenant's database.
³ Lucid on MySQL supports database-per-tenant only. Its row-level and schema-per-tenant strategies
are PostgreSQL-only (they rely on forced RLS / search_path), but database-per-tenant is pure
connection-routing, so it works on MySQL too - proven by a two-tenant adversarial test.
By adapter × strategy
| Adapter | Database | Row-level | Schema-per-tenant | Database-per-tenant |
|---|---|---|---|---|
| Knex | PostgreSQL | ✅ | ✅ | ✅ |
| Lucid (AdonisJS) | PostgreSQL | ✅ | ✅ | ✅ |
| Lucid (AdonisJS) | MySQL | - | - | ✅ |
| Prisma | PostgreSQL | ✅ | ✅ | ✅ |
| Prisma | MySQL | 🧪 | - | ✅ |
| TypeORM | PostgreSQL | ✅ | ✅ | ✅ |
| Sequelize | PostgreSQL | ✅ | ✅ | ✅ |
| TypeORM | MySQL | 🧪 | - | ✅ |
| Sequelize | MySQL | 🧪 | - | ✅ |
| Drizzle | PostgreSQL | ✅ | ✅ | ✅ |
| Drizzle | MySQL | 🧪 | - | ✅ |
| Mongoose | MongoDB | ✅ ¹ | - | ✅ ² |
✅ tested-supported · - not available yet
Query freedom by tier
Whether a scope allows raw SQL, joins, and nested reads/writes depends on how it's isolated, not just which combination you picked (ADR-0033):
| Tier | Isolated by | Raw / joins / nested | Available today |
|---|---|---|---|
| Database-enforced | the connection or database itself | ✅ full freedom | every adapter's database-per-tenant scope |
| Facade-enforced | the adapter facade only | rejected (fail-closed) ³ | every other scope |
In a database-per-tenant scope the leased connection is the tenant's own database, so raw SQL,
joins, and nested reads/writes are safe by construction. Every adapter exposes the raw tenant-scoped
client there via client.unrestricted() - Knex, TypeORM, Sequelize, Drizzle, Mongoose, and Lucid (as
scope.unrestricted()). Prisma database-per-tenant already hands your callback the raw leased
PrismaClient directly, so it has full freedom without a separate accessor.
³ Lucid is the one facade-enforced exception: its facade supports nested reads (joins/relations), though raw queries and nested writes are still rejected. See Limitations.
Database-enforced freedom is fail-closed: the raw client is handed over only where the database
itself binds every statement to the tenant - a leased per-tenant connection (database-per-tenant, tenant
mode) or forced-RLS row-level on PostgreSQL in tenant mode (the validated policy under a
non-BYPASSRLS role). It throws in facade-enforced scopes: MySQL row-level, schema-per-tenant, and
central mode.
How each is enforced
- PostgreSQL, row-level - Knex, Lucid, TypeORM, Sequelize, and Drizzle use forced Postgres
RLS (the database rejects cross-tenant rows even under raw SQL) plus the adapter's facade.
Prisma has two options: its default extension path is query-rewriting facade only (no database
backstop), and an RLS-backed path
(
createPrismaRowLevelTenancy) that adds forced RLS. See Security. - PostgreSQL, schema-per-tenant - Knex, Lucid, TypeORM, Sequelize, and Drizzle use a
transaction-local
search_path; an optional per-tenant role makes the database reject sibling-schema access. Prisma uses a cached Prisma 7 client whose PostgreSQL driver adapter is explicitly bound to the tenant schema; credential restriction is required for a database-enforced guarantee. - MySQL, row-level (🧪 experimental) - Prisma, TypeORM, Sequelize, and Drizzle use protected
query-scoping facades. MySQL has no row-level security, so
- like Mongoose - isolation is enforced entirely by the adapter facade, with no database-level backstop. It's a weaker guarantee than Postgres row-level (which the database also enforces); keep all tenant access going through the adapter.
- MongoDB, row-level - the Mongoose adapter's query facade (see the caveat above).
- Database-per-tenant - separate PostgreSQL/MySQL databases or MongoDB databases, cache-routed per tenant. Credential scope determines whether this is only a routing boundary or also a database authorization boundary.
What's not there yet
- Prisma schema-per-tenant does not use
search_path. That approach remains rejected. Prisma 7's PostgreSQL driver adapter now supplies an explicit schema binding, and TenancyJS cache-routes one callback-scoped client per tenant schema. - MySQL, schema-per-tenant isn't a thing - in MySQL, "schema" and "database" are synonyms, so
there's no
search_pathnamespace to switch. The Postgres schema-per-tenant concept simply maps to database-per-tenant in MySQL terms. - Drizzle does not target MongoDB. It is a SQL ORM; Mongoose remains the MongoDB adapter.
- MongoDB schema-per-tenant is not available because MongoDB has no schema/search-path namespace.
Roadmap
Rough order; this section is honest about what's shipped and what's coming.
| Item | What it adds | Status |
|---|---|---|
| Full query freedom → PostgreSQL forced-RLS | unrestricted() in row-level scopes where forced RLS + a non-bypass role isolate every query | ✅ Shipped |
| RLS-backed Prisma row-level | createPrismaRowLevelTenancy - a forced-RLS database backstop for Prisma | ✅ Shipped |
| Full query freedom → schema + role | database-enforced freedom in schema-per-tenant scopes with a restricted per-tenant role | 📋 Planned |
| Microsoft SQL Server | a SQL Server adapter (row-level via SESSION_CONTEXT + security policies, and database-per-tenant) | 📋 Planned - later in the pipeline |
✅ shipped · 📋 planned
Full query freedom via unrestricted() is now shipped in every database-enforced scope -
database-per-tenant and forced-RLS row-level on PostgreSQL. The remaining roadmap item extends it to
schema-per-tenant with a restricted per-tenant role, which is lower priority (that scope already isolates
every query, so it's ergonomics, not a capability gap).
Microsoft SQL Server is on the roadmap but not yet available - there is no SQL Server adapter today. It sits later in the pipeline behind the PostgreSQL, MySQL, and MongoDB work. Track it on the repository.
The CLI tells you too
You don't have to memorise this. tenancy tenant check reads each configured adapter's own capability
self-report and warns you - to your face - about any combination that isn't tested-supported:
WARN adapter:mongoose: schemaPerTenant is reported as "rejected", not
tested-supported - this combination is not in the verified matrix;
use at your own risk