TenancyJS
Core Concepts

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.

DatabaseRow-levelSchema-per-tenantDatabase-per-tenantAdapters
PostgreSQLKnex · 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

AdapterDatabaseRow-levelSchema-per-tenantDatabase-per-tenant
KnexPostgreSQL
Lucid (AdonisJS)PostgreSQL
Lucid (AdonisJS)MySQL--
PrismaPostgreSQL
PrismaMySQL🧪-
TypeORMPostgreSQL
SequelizePostgreSQL
TypeORMMySQL🧪-
SequelizeMySQL🧪-
DrizzlePostgreSQL
DrizzleMySQL🧪-
MongooseMongoDB✅ ¹-✅ ²

✅ 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):

TierIsolated byRaw / joins / nestedAvailable today
Database-enforcedthe connection or database itself✅ full freedomevery adapter's database-per-tenant scope
Facade-enforcedthe adapter facade onlyrejected (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 when a per-tenant connection was genuinely leased (database-per-tenant, tenant mode), and throws in row-level, schema-per-tenant, and central scopes. Row-level (forced RLS) and schema-per-tenant (restricted role) are also database-enforced in principle, but freeing raw queries there is not implemented - see the roadmap.

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 is the exception: it has no RLS layer - its PostgreSQL (and MySQL) row-level isolation is the adapter's query-rewriting facade only, with no database backstop. 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_path namespace 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

Planned work, in rough order. None of this is available yet - this section is honest about what's coming so you can plan around it.

ItemWhat it addsStatus
Full query freedom → PostgreSQL forced-RLSdatabase-enforced freedom in row-level scopes where forced RLS + a non-bypass role isolate every query📋 Planned
Full query freedom → schema + roledatabase-enforced freedom in schema-per-tenant scopes with a restricted per-tenant role📋 Planned
Microsoft SQL Servera SQL Server adapter (row-level via SESSION_CONTEXT + security policies, and database-per-tenant)📋 Planned - later in the pipeline

📋 planned

Full query freedom in database-per-tenant is shipped on every adapter - the roadmap items above would extend it to the shared-connection tiers (row-level with forced RLS, schema-per-tenant with a restricted role), which are lower priority: those scopes already isolate every query, so this is ergonomics rather than 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

On this page