@kosan/prisma
Prisma adapter for Kosan. Creates one PrismaClient per tenant with a per-tenant datasource URL, and provides PrismaMasterStore for the master tenant table.
npm install @kosan/prismaNo hard dependency on @prisma/client — the adapter is fully generic and uses structural typing. Your generated client is a peer dependency.
Setup
1 — Add the tenant model to your Prisma schema
model Tenant {
id String @id @default(uuid())
slug String @unique
host String
port Int
dbName String
user String
password String
status String @default("active")
meta Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Or copy the convenience export:
import { TENANT_PRISMA_SCHEMA } from '@kosan/prisma';
console.log(TENANT_PRISMA_SCHEMA); // prints the Prisma model block2 — Create the adapter and master store
import { PrismaClient } from '@prisma/client';
import { PrismaAdapter, PrismaMasterStore } from '@kosan/prisma';
import { TenantRegistry } from '@kosan/core';
// Master database client
const masterPrisma = new PrismaClient();
const masterStore = new PrismaMasterStore(masterPrisma.tenant);
const adapter = new PrismaAdapter({
PrismaClient, // constructor for the generated client
buildUrl: (tenant) =>
`postgresql://${tenant.user}:${tenant.password}@${tenant.host}:${tenant.port}/${tenant.dbName}`,
datasourceName: 'db', // matches `datasource db { }` in schema.prisma
});
export const registry = await TenantRegistry.create({
master: masterStore,
adapter,
});PrismaAdapter
Options
interface PrismaAdapterOptions<TClient> {
PrismaClient: PrismaClientConstructor<TClient>;
buildUrl: (tenant: TenantConfig) => string;
datasourceName?: string; // default: 'db'
}buildUrl receives the decrypted TenantConfig and must return a full connection string. This is where you can inject dynamic credentials, proxy URLs, or PgBouncer addresses:
buildUrl: (tenant) => {
const base = `postgresql://${tenant.user}:${tenant.password}@`;
if (tenant.meta?.['region'] === 'eu') {
return `${base}eu-proxy.internal:6432/${tenant.dbName}`;
}
return `${base}${tenant.host}:${tenant.port}/${tenant.dbName}`;
},PrismaMasterStore
Pass prisma.tenant (the generated delegate) to the constructor. The store uses structural typing — as long as your Tenant model has the expected fields, it works:
import { PrismaMasterStore } from '@kosan/prisma';
const masterStore = new PrismaMasterStore(masterPrisma.tenant);Accessing the Prisma client in handlers
Use the usePrisma<TClient>() shortcut instead of casting models manually:
import { usePrisma } from '@kosan/prisma';
import type { PrismaClient } from '@prisma/client';
app.get('/users', async (_req, res) => {
const prisma = usePrisma<PrismaClient>();
const users = await prisma.user.findMany();
res.json(users);
});Under the hood this reads useTenant().models['prisma'] with a type-safe cast.
TENANT_PRISMA_SCHEMA
A convenience string with the ready-to-paste Prisma schema block:
import { TENANT_PRISMA_SCHEMA } from '@kosan/prisma';
// Paste into your schema.prisma or print for referenceMultiple schemas per tenant
If each tenant has their own Prisma schema (uncommon but valid), generate multiple clients and switch the PrismaClient constructor per tenant using meta:
import { PrismaClientV1 } from '../generated/v1/index.js';
import { PrismaClientV2 } from '../generated/v2/index.js';
const adapter = new PrismaAdapter({
PrismaClient: PrismaClientV2, // default
buildUrl: (tenant) => {
const client = tenant.meta?.['schemaVersion'] === 'v1'
? PrismaClientV1
: PrismaClientV2;
// For per-tenant constructor switching, write a custom Adapter instead.
return `postgresql://${tenant.user}:${tenant.password}@${tenant.host}/${tenant.dbName}`;
},
});For complex per-tenant schema versioning, implement a custom Adapter<PrismaClient> (see Writing an adapter).
Testing without the generated client
Use a structural fake that satisfies PrismaClientLike:
import type { PrismaClientLike } from '@kosan/prisma';
class FakePrismaClient implements PrismaClientLike {
async $connect() {}
async $disconnect() {}
user = {
findMany: async () => [{ id: '1', email: 'test@example.com' }],
};
}