TypeORM
Protected TypeORM repository isolation for PostgreSQL and MySQL.
tenancyjs-adapter-typeorm provides all three strategies on PostgreSQL plus adapter-enforced row-level
and database-per-tenant on MySQL. This page shows it wired
end to end with Express; for another framework, swap only the integration
(see below).
Install
npm install tenancyjs-core tenancyjs-adapter-typeorm tenancyjs-integration-express typeormWire it into your app
Create the manager + adapter
import { TenancyManager } from "tenancyjs-core";
import { createTypeOrmTenancy } from "tenancyjs-adapter-typeorm";
import { dataSource, Order } from "./data-source";
export interface Tenant {
readonly id: string;
}
export const manager = new TenancyManager<Tenant>();
export const tenancy = createTypeOrmTenancy({
manager,
dataSource,
tenantEntities: [
{
entity: Order,
table: "app.orders",
tenantProperty: "tenantId",
tenantColumn: "tenant_id",
},
],
});
await tenancy.validate();Bind it to requests
tenancy.run gives you a narrow protected client-not a native EntityManager or repository.
import express from "express";
import { createExpressTenancyMiddleware } from "tenancyjs-integration-express";
import { manager, tenancy } from "./tenancy";
import { Order } from "./data-source";
const app = express();
app.use(
createExpressTenancyMiddleware({
manager,
resolver, // a TenantResolutionChain (tenancyjs-identifiers) - see the Express guide
}),
);
app.get("/orders", async (_req, res) => {
const orders = await tenancy.run((client) =>
client.repository(Order).findBy({ status: "open" }),
);
res.json(orders); // only the current tenant's rows
});Other strategies
Schema mode requires unqualified entity metadata and table config:
createTypeOrmTenancy({
manager,
dataSource,
strategy: "schemaPerTenant",
schema: (tenant) => tenant.schemaName,
tenantEntities: [{ entity: Order, table: "orders" }],
});Database mode leases initialized tenant DataSource objects from the bounded cache:
createTypeOrmTenancy({
manager,
dataSource: landlordDataSource,
strategy: "databasePerTenant",
tenantEntities: [{ entity: Order, table: "orders" }],
connection: (tenant) => ({
key: tenant.databaseKey,
create: () => createTenantDataSource(tenant.databaseSecretRef),
}),
});Call validate() before serving traffic and close() during shutdown. Native managers, query
builders, relations, raw SQL, schema sync, and migrations remain outside the protected surface.
For MySQL, pass dialect: "mysql". Row isolation is experimental and adapter-enforced because MySQL
has no RLS. MySQL schema and database are synonyms, so use databasePerTenant instead of schema mode.
Full query freedom: unrestricted()
unrestricted() returns a tenant-scoped EntityManager - full query builder, relations, and raw SQL.
It's available in the two scopes where the database itself keeps a query inside the tenant:
database-per-tenant (the leased connection is the tenant's own database) and forced-RLS
row-level on PostgreSQL (the scoped EntityManager runs under a non-BYPASSRLS role bound by the
validated RLS policy, so it cannot cross tenants). It's still refused in schema-per-tenant, central
mode, and MySQL row-level (no RLS backstop) - there the protected client is the only guard, so it can't
let through what it can't prove is tenant-safe (see Limitations):
const report = await tenancy.run(async (client) => {
// The raw, tenant-scoped EntityManager - query builder, relations, raw SQL.
const manager = client.unrestricted();
return manager
.createQueryBuilder(Order, "order")
.innerJoin("order.customer", "customer")
.where("order.total > :min", { min: 1000 })
.select(["order.id", "customer.name"])
.getRawMany();
});unrestricted() is fail-closed. It returns a real EntityManager only where the database
itself enforces the tenant boundary - a database-per-tenant config in tenant mode (a per-tenant
connection was actually leased), or forced-RLS row-level on PostgreSQL (the scoped manager is bound by
the validated policy under a non-BYPASSRLS role). It throws in schema-per-tenant scopes, in MySQL
row-level (no RLS backstop), and in any config used in central mode (which runs on the shared admin
connection, not a tenant's database). The freedom comes from a database boundary, never from the
config name
(ADR-0038).
Use a different framework
Keep the adapter half; only swap the integration import: