TenancyJS
GuidesProvisioning per ORM

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.

provisioning.ts
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.

tenancy.config.ts
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 it

Roll 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 from prisma.config.ts. The ?schema= URL still selects the schema for migrate deploy.
  • Keep provision idempotent (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.

On this page