Database
`@multitenant/database` — ALS tenant scope, URL resolution, bounded pool cache, Postgres helpers.
Node-only. This package gives you AsyncLocalStorage so repository code can read tenantKey without threading it through every function, plus bounded caching for per-tenant DB clients/pools, env-based DSN resolution from config, and small Postgres helpers (RLS GUC, tenant_id on writes).
It does not resolve the tenant from HTTP — use @multitenant/core + an adapter first, then call runWithTenantScope at the boundary.
Install
npm install @multitenant/database1. Tenant scope (ALS)
After you have ResolvedTenant (e.g. from registry.resolveByRequest), store tenantKey and optionally the full resolved object for downstream code:
import type { TenantRegistry } from '@multitenant/core';
import { runWithTenantScopeAsync, requireTenantKey, assignTenantIdForWrite } from '@multitenant/database';
export async function handleRequest(registry: TenantRegistry, req: Request) {
const resolved = registry.resolveByRequest(req as any, { environment: 'local' });
if (!resolved) return new Response('no tenant', { status: 404 });
return runWithTenantScopeAsync({ tenantKey: resolved.tenantKey, resolved }, async () => {
const key = requireTenantKey(); // same as resolved.tenantKey
const row = assignTenantIdForWrite({ title: 'hello' }); // sets tenant_id = key
// ... db access that reads requireTenantKey() / getResolvedTenantFromScope()
return new Response(JSON.stringify({ key, row }));
});
}Helpers: getTenantScope, requireTenantScope, requireResolvedTenantFromScope, requireTenantKey, getResolvedTenantFromScope.
Shared-database topology: one physical DB for all tenants — use assignTenantIdForWrite / assertRowTenantColumn and/or Postgres RLS (buildSetLocalTenantGucSql, buildSetLocalTenantGucSqlFromScope) so rows cannot cross tenants.
2. Resolve DSN from config + env
In tenants.config.json, each tenant may declare database.envVar (the name of an env var holding the URL, not the secret in JSON). resolveTenantDatabaseUrl reads it:
import type { ResolvedTenant, TenantDefinition } from '@multitenant/core';
import { resolveTenantDatabaseUrl } from '@multitenant/database';
export function databaseUrlForTenant(
resolved: ResolvedTenant,
tenants: Record<string, TenantDefinition>,
): string | undefined {
return resolveTenantDatabaseUrl(resolved, tenants, { required: true });
}If required: true (default) and the env var is missing, it throws with a clear message.
3. Bounded pool / client cache
BoundedTenantDbResourceCache<T> stores one resource per (tenantKey, database URL) so you do not leak pools when many tenants exist. Use onEvict to pool.end(), $disconnect(), or dataSource.destroy() when entries are evicted.
import { BoundedTenantDbResourceCache } from '@multitenant/database';
import { Pool } from 'pg';
const poolCache = new BoundedTenantDbResourceCache<Pool>({
maxPools: 64,
idleEvictMs: 30 * 60 * 1000,
onEvict: (pool) => {
void pool.end();
},
});ORM packages (@multitenant/drizzle, Kysely, Prisma, TypeORM) wrap this cache with driver-specific factories — see ORM helpers.
4. Postgres schema / RLS
schemaNameForTenant,requireSchemaNameForCurrentTenant— safe identifier length / naming for per-tenant schemas.buildSetLocalTenantGucSql,buildSetLocalTenantGucSqlFromScope— emitSET LOCALfor a custom GUC consumed by RLS policies.
See also
@multitenant/identity— encrypted session cookies; pair with host resolution.- Drizzle, Kysely, Prisma, TypeORM — per-tenant or shared DB facades.