Tenant-Isolated Databases¶
Multi-tenant setup where each tenant has their own isolated database.
Overview¶
For multi-tenant applications where each tenant has their own isolated database, the IdentitySyncHandler supports a resolver function pattern.
Single Database vs Tenant-Isolated¶
import { IdentitySyncHandler } from '@authvital/sdk/server';
import { PrismaClient } from '@prisma/client';
// ═══════════════════════════════════════════════════════════════════════════
// OPTION 1: Single Database (shared or single-tenant)
// ═══════════════════════════════════════════════════════════════════════════
const prisma = new PrismaClient();
const handler = new IdentitySyncHandler(prisma);
// ═══════════════════════════════════════════════════════════════════════════
// OPTION 2: Tenant-Isolated Databases
// ═══════════════════════════════════════════════════════════════════════════
// Pass a resolver function that returns the Prisma client for a given tenant
const handler = new IdentitySyncHandler((tenantId: string) => {
return getTenantPrisma(tenantId);
});
Full Example: Tenant-Isolated Setup¶
Tenant Prisma Client Factory¶
// lib/tenant-prisma.ts
import { PrismaClient } from '@prisma/client';
// Cache Prisma clients per tenant to avoid creating new connections
const tenantClients = new Map<string, PrismaClient>();
export function getTenantPrisma(tenantId: string): PrismaClient {
// Check cache first
let client = tenantClients.get(tenantId);
if (client) return client;
// Get tenant's database URL from your tenant registry
const databaseUrl = getTenantDatabaseUrl(tenantId);
// Create new Prisma client for this tenant
client = new PrismaClient({
datasources: {
db: { url: databaseUrl },
},
});
tenantClients.set(tenantId, client);
return client;
}
function getTenantDatabaseUrl(tenantId: string): string {
// This could come from:
// - Environment variables: process.env[`DATABASE_URL_${tenantId}`]
// - A tenant registry database
// - A configuration service
// - etc.
// Example: Each tenant has their own database
return `postgresql://user:pass@host:5432/tenant_${tenantId}`;
}
Webhook Handler Setup¶
// webhooks/authvital.ts
import { IdentitySyncHandler, WebhookRouter } from '@authvital/sdk/server';
import { getTenantPrisma } from '../lib/tenant-prisma';
// The handler will call this function with the tenantId from each webhook event
const syncHandler = new IdentitySyncHandler((tenantId) => getTenantPrisma(tenantId));
const router = new WebhookRouter({
handler: syncHandler,
authVitalHost: process.env.AV_HOST!,
});
export default router;
How Tenant Resolution Works¶
- Webhook event arrives with
tenant_idin the payload IdentitySyncHandlercalls your resolver function with thattenant_id- Your resolver returns the appropriate Prisma client for that tenant
- The handler uses that client to perform the database operation
- The identity is synced to the correct tenant database
┌─────────────┐ ┌──────────────────────┐
│ AuthVital │ │ Your Webhook API │
│ Webhook │ ──────────▶ │ IdentitySyncHandler │
│ (tenant_id │ │ │
│ = "acme") │ └──────────────────────┘
└─────────────┘ │
│ resolver("acme")
▼
┌──────────────────────────────────┐
│ getTenantPrisma │
│ returns Prisma client for ACME │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ ACME's Isolated Database │
│ (av_identities table) │
└──────────────────────────────────┘
Async Resolvers¶
The resolver can also be async if you need to fetch tenant configuration from a database or service:
const syncHandler = new IdentitySyncHandler(async (tenantId: string) => {
// Fetch tenant config from your registry
const tenant = await tenantRegistry.findById(tenantId);
if (!tenant) {
throw new Error(`Unknown tenant: ${tenantId}`);
}
return getTenantPrisma(tenant.databaseUrl);
});
Connection Pool Management¶
For large numbers of tenants, consider implementing connection pool limits:
// lib/tenant-prisma.ts
import { PrismaClient } from '@prisma/client';
const MAX_CACHED_CLIENTS = 100;
const tenantClients = new Map<string, {
client: PrismaClient;
lastUsed: number;
}>();
export async function getTenantPrisma(tenantId: string): Promise<PrismaClient> {
// Check cache
const cached = tenantClients.get(tenantId);
if (cached) {
cached.lastUsed = Date.now();
return cached.client;
}
// Evict oldest if at limit
if (tenantClients.size >= MAX_CACHED_CLIENTS) {
const oldest = [...tenantClients.entries()]
.sort((a, b) => a[1].lastUsed - b[1].lastUsed)[0];
await oldest[1].client.$disconnect();
tenantClients.delete(oldest[0]);
}
// Create new client
const databaseUrl = await getTenantDatabaseUrl(tenantId);
const client = new PrismaClient({
datasources: { db: { url: databaseUrl } },
});
tenantClients.set(tenantId, { client, lastUsed: Date.now() });
return client;
}
Error Handling¶
If a tenant's database is unavailable, handle gracefully:
const syncHandler = new IdentitySyncHandler(async (tenantId: string) => {
try {
return await getTenantPrisma(tenantId);
} catch (error) {
console.error(`Failed to connect to tenant ${tenantId} database:`, error);
// Option 1: Re-throw to fail the webhook (will retry)
throw error;
// Option 2: Return a fallback/default database
// return getDefaultPrisma();
}
});