Prisma - provisioning
Create and migrate each tenant's schema or database with Prisma, driven by the tenancy CLI.
This recipe implements the provisioner hooks for Prisma so
tenancy tenant provision | migrate | deprovision create, migrate, and drop each tenant's placement.
Routing (leasing the tenant's client at request time) is on the Prisma adapter page;
this page is only the setup seam TenancyJS leaves to you.
Prisma has no programmatic migrate API, so the migrate hook runs prisma migrate deploy with a
per-tenant DATABASE_URL. DDL runs on a privileged pg pool, never your fail-closed runtime role.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { Pool } from "pg";
const run = promisify(execFile);
// Admin/maintenance connection allowed to create schemas and databases.
const admin = new Pool({ connectionString: process.env.ADMIN_DATABASE_URL });
const schemaOf = (id: string) => `tenant_${id}`; // your placement naming
const dbOf = (id: string) => `tenant_${id}`;
// `migrate deploy` applies committed migrations; DATABASE_URL selects the target.
async function prismaDeploy(databaseUrl: string): Promise<void> {
await run("npx", ["prisma", "migrate", "deploy"], {
env: { ...process.env, DATABASE_URL: databaseUrl },
});
}Schema-per-tenant (PostgreSQL)
One Postgres schema per tenant in a shared database. Prisma selects the schema with the ?schema=
connection-string parameter.
import { defineTenancyRuntime } from "tenancyjs-core";
const baseUrl = process.env.DATABASE_URL!; // e.g. postgres://…/app
export default defineTenancyRuntime({
manager,
store,
adapters: [tenancy], // createPrismaSchemaTenancy(...) - see the adapter page
provisioner: {
provision: async (tenant) => {
await admin.query(`create schema if not exists "${schemaOf(tenant.id)}"`);
},
migrate: async (tenant) => {
const url = new URL(baseUrl);
url.searchParams.set("schema", schemaOf(tenant.id));
await prismaDeploy(url.toString());
},
deprovision: async (tenant) => {
await admin.query(`drop schema if exists "${schemaOf(tenant.id)}" cascade`);
},
},
dispose: () => admin.end(),
});Database-per-tenant
A separate database per tenant. CREATE DATABASE cannot run inside a transaction and must target a
maintenance database (e.g. postgres), which is what the admin pool connects to.
provisioner: {
provision: async (tenant) => {
// Identifier is quoted; the id is validated by the store, but keep placement
// names to [a-z0-9_] so quoting is the only escaping you rely on.
await admin.query(`create database "${dbOf(tenant.id)}"`);
},
migrate: async (tenant) => {
await prismaDeploy(connectionStringFor(dbOf(tenant.id))); // your per-db URL
},
deprovision: async (tenant) => {
await admin.query(`drop database if exists "${dbOf(tenant.id)}" with (force)`);
},
},On MySQL, CREATE DATABASE "tenant_x" / DROP DATABASE is the same shape - swap the pg pool for
a mysql2 admin connection and use `tenant_x` backtick quoting.
Run the flow
npx tenancy tenant create acme --set plan=pro # 1. record + placement
npx tenancy tenant provision acme # 2. create schema/database
npx tenancy tenant migrate acme # 3. prisma migrate deploy
npx tenancy test:leak --test-file ./leak.mjs # 4. prove isolation before trusting itRoll a new migration out to everyone with npx tenancy tenant migrate --all - it reports each
tenant's outcome and exits non-zero if any fail, so it's safe in CI.
Notes
- Prisma 7 needs a driver adapter (
@prisma/adapter-pg) and reads the datasource fromprisma.config.ts. The?schema=URL still selects the schema formigrate deploy. - Keep
provisionidempotent (if not exists) - the CLI may retry, and a half-onboarded tenant should be safe to re-provision. - Routing validates the schema/database already exists; a tenant that hasn't been provisioned +
migrated fails closed on its first request rather than lazily creating storage. Provision before you
flip the tenant to
active.