First-class multi-tenancy with database-per-tenant support for Node.js — the layer that Sequelize, Prisma, and Drizzle don't ship.
Every major Node.js ORM requires you to hand-roll the same things:
- A connection manager that opens one pool per tenant
- A tenant resolver tied to your HTTP framework
- Request-scoped context propagation (no prop-drilling)
- LRU eviction and idle-connection cleanup
- Per-tenant migration orchestration
Kosan does all of this for you, with a clean adapter model so you keep using the ORM you already know.
| Package | Description |
|---|---|
@kosan/core |
ORM-agnostic registry, cache, context, resolvers |
@kosan/sequelize |
Sequelize v6 adapter |
@kosan/prisma |
Prisma adapter |
@kosan/drizzle |
Drizzle ORM adapter |
@kosan/express |
Express middleware |
@kosan/fastify |
Fastify plugin (v4 & v5) |
@kosan/koa |
Koa middleware |
@kosan/nestjs |
NestJS module |
@kosan/cli |
Migration orchestrator CLI |
npm install @kosan/core @kosan/sequelize @kosan/expressimport { TenantRegistry, SubdomainResolver, useTenant } from '@kosan/core';
import { SequelizeAdapter, SequelizeMasterStore } from '@kosan/sequelize';
import { tenantMiddleware } from '@kosan/express';
import { Sequelize, DataTypes } from 'sequelize';
import express from 'express';
// 1 — Connect to the master database
const master = new Sequelize('postgres://admin:pass@localhost/master');
const masterStore = await SequelizeMasterStore.create(master);
const registry = await TenantRegistry.create({
master: masterStore,
adapter: new SequelizeAdapter({ defaultDialect: 'postgres' }),
hooks: {
async onCreate(tenant, conn) {
await conn.sync(); // create tables in the new tenant's database
},
},
});
// 2 — Register models (factory called once per tenant connection)
registry.registerModels([
function OrderModel(sequelize: Sequelize) {
return sequelize.define('Order', {
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
total: { type: DataTypes.DECIMAL },
});
},
]);
// 3 — Wire up Express middleware
const app = express();
app.use(tenantMiddleware({ registry, resolver: new SubdomainResolver() }));
// 4 — Use models in route handlers — no prop-drilling
app.get('/orders', async (_req, res) => {
const { models } = useTenant();
res.json(await (models.Order as any).findAll());
});
app.listen(3000);npm install @kosan/core @kosan/prisma @kosan/koaimport { TenantRegistry, SubdomainResolver, useTenant } from '@kosan/core';
import { PrismaAdapter, PrismaMasterStore, usePrisma } from '@kosan/prisma';
import { tenantMiddleware } from '@kosan/koa';
import { PrismaClient } from '@prisma/client';
import Koa from 'koa';
const masterPrisma = new PrismaClient();
const registry = await TenantRegistry.create({
master: new PrismaMasterStore(masterPrisma.tenant),
adapter: new PrismaAdapter({
PrismaClient,
buildUrl: (t) => `postgresql://${t.user}:${t.password}@${t.host}:${t.port}/${t.dbName}`,
}),
});
const app = new Koa();
app.use(tenantMiddleware({ registry, resolver: new SubdomainResolver() }));
app.use(async (ctx) => {
const prisma = usePrisma<PrismaClient>();
ctx.body = await prisma.order.findMany();
});npm install @kosan/core @kosan/drizzle @kosan/fastify drizzle-orm postgresimport { TenantRegistry, SubdomainResolver } from '@kosan/core';
import { DrizzleAdapter, DrizzleMasterStore, useDrizzle } from '@kosan/drizzle';
import tenantPlugin from '@kosan/fastify';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import Fastify from 'fastify';
import * as schema from './schema.js';
type TenantDb = ReturnType<typeof drizzle<typeof schema>>;
const registry = await TenantRegistry.create({
master: new DrizzleMasterStore({ /* query callbacks */ }),
adapter: new DrizzleAdapter<TenantDb>({
clientFactory: (t) =>
drizzle(postgres(`postgres://${t.user}:${t.password}@${t.host}:${t.port}/${t.dbName}`), { schema }),
}),
});
const fastify = Fastify();
await fastify.register(tenantPlugin, { registry, resolver: new SubdomainResolver() });
fastify.get('/orders', async () => {
const db = useDrizzle<TenantDb>();
return db.select().from(schema.orders);
});| Resolver | Extracts tenant from |
|---|---|
SubdomainResolver |
acme.myapp.com → acme |
HeaderResolver('X-Tenant-ID') |
X-Tenant-ID: acme header |
PathResolver({ segment: 0 }) |
/acme/orders → acme |
| Custom function | (req) => req.user?.tenantId |
TenantRegistry.create({
// ...
hooks: {
async onCreate(tenant, conn) {
// provision the physical database, run initial migrations, etc.
},
async onDelete(tenant) { /* archive or drop */ },
async onSuspend(tenant) { /* notify downstream services */ },
},
});npx kosan migrate --config kosan.config.ts --concurrency 4| Example | Stack |
|---|---|
sequelize-express |
Sequelize + Express |
sequelize-nestjs |
Sequelize + NestJS |
drizzle-fastify |
Drizzle + Fastify |
prisma-koa |
Prisma + Koa |
- Node.js ≥ 20
- TypeScript ≥ 5 (strict mode recommended)
pnpm install
pnpm test # run all tests
pnpm build # build all packages
pnpm typecheck # type-check all packages
pnpm lint # Biome lintReleasing — see RELEASING.md.
MIT