Multi-Tenancy
VentureKit provides multi-tenancy support through @venturekit-pro/tenancy with flexible tenant resolution, data isolation, and quota enforcement.
npm install @venturekit-pro/tenancy@devTenant Resolution Strategies
Section titled “Tenant Resolution Strategies”VentureKit supports multiple ways to identify the current tenant:
| Strategy | Example | Use Case |
|---|---|---|
subdomain | acme.app.example.com | SaaS with subdomains |
custom_domain | app.acme.com | White-label support (requires domainLookup) |
path | /t/acme/api/tasks | Shared domain |
header | X-Tenant-ID: acme | API-first apps (configurable via headerName) |
query | ?tenant=acme | Webhooks, ad-hoc routing (via queryParam) |
jwt | tenant_id claim | Token-based (configurable via jwtClaim) |
custom | pluggable async resolver | Anything else (via customResolver) |
Adding Tenant Middleware
Section titled “Adding Tenant Middleware”import { handler } from '@venturekit/runtime';import { createTenantMiddleware } from '@venturekit-pro/tenancy';import type { IsolationConfig, Tenant } from '@venturekit-pro/tenancy';
// `strategy` is the **data** isolation model (shared tables, schema-per-tenant,// database-per-tenant, or hybrid). `resolution` is how the tenant is identified// from the incoming request.const tenantConfig: IsolationConfig = { strategy: 'shared', resolution: 'subdomain',};
// Lookup function returns the resolved tenant (or null) given the resolved idasync function lookupTenant(tenantId: string): Promise<Tenant | null> { return loadTenant(tenantId);}
export const main = handler(async (_body, ctx, logger) => { const tenant = ctx.tenant!; logger.info('Request for tenant', { tenantId: tenant.id, slug: tenant.slug });
return { tenantId: tenant.id };}, { scopes: ['api.read'], middleware: [ createTenantMiddleware({ config: tenantConfig, lookupTenant }), ],});Quota Enforcement
Section titled “Quota Enforcement”Enforce per-tenant usage limits:
import { createQuotaMiddleware, checkQuotas } from '@venturekit-pro/tenancy';
// As middleware (automatic per-request) — must specify which quota to enforce// and how to read current usage. Throws QuotaExceededError when over budget.export const main = handler(async (_body, ctx, logger) => { return { ok: true };}, { scopes: ['api.read'], middleware: [ createTenantMiddleware({ config: tenantConfig, lookupTenant }), createQuotaMiddleware({ quotaKey: 'apiRequests', checkUsage: async (tenantId) => getMonthlyUsage(tenantId, 'apiRequests'), }), ],});
// Programmatic check — (tenantId, quotas, checkUsage)// quotas: TenantQuotas — a record of quotaKey → numeric limit// checkUsage: (tenantId, quotaKey) => Promise<number>await checkQuotas( tenantId, { apiRequests: 10_000, storage: 5_000_000_000 }, async (id, key) => loadCurrentUsage(id, key),);Tenant Context
Section titled “Tenant Context”Access tenant information anywhere in your handler:
import { getCurrentTenant } from '@venturekit-pro/tenancy';
const tenant = getCurrentTenant(ctx);// { id: 'acme', slug: 'acme', metadata: { plan: 'pro', ... } }Error Handling
Section titled “Error Handling”The tenancy package provides specific error types:
import { TenantNotFoundError, TenantSuspendedError, TenantInactiveError, QuotaExceededError,} from '@venturekit-pro/tenancy';These errors are automatically caught by the error boundary and returned as structured JSON responses with appropriate HTTP status codes.
WebSocket Multi-Tenancy
Section titled “WebSocket Multi-Tenancy”For real-time WebSocket apps, pass tenantId during authentication:
{ "action": "auth", "token": "<jwt>", "tenantId": "acme" }Then use the connectionStore for tenant-scoped messaging:
import { connectionStore } from '@venturekit/runtime/ws';
// Broadcast within a tenantawait connectionStore.sendToTenant(domainName, stage, tenantId, data);
// Query tenant connectionsconst connections = await connectionStore.getByTenant(tenantId);