Skip to content

Multi-Tenancy

VentureKit provides multi-tenancy support through @venturekit-pro/tenancy with flexible tenant resolution, data isolation, and quota enforcement.

Terminal window
npm install @venturekit-pro/tenancy@dev

VentureKit supports multiple ways to identify the current tenant:

StrategyExampleUse Case
subdomainacme.app.example.comSaaS with subdomains
custom_domainapp.acme.comWhite-label support (requires domainLookup)
path/t/acme/api/tasksShared domain
headerX-Tenant-ID: acmeAPI-first apps (configurable via headerName)
query?tenant=acmeWebhooks, ad-hoc routing (via queryParam)
jwttenant_id claimToken-based (configurable via jwtClaim)
custompluggable async resolverAnything else (via customResolver)
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 id
async 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 }),
],
});

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),
);

Access tenant information anywhere in your handler:

import { getCurrentTenant } from '@venturekit-pro/tenancy';
const tenant = getCurrentTenant(ctx);
// { id: 'acme', slug: 'acme', metadata: { plan: 'pro', ... } }

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.

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 tenant
await connectionStore.sendToTenant(domainName, stage, tenantId, data);
// Query tenant connections
const connections = await connectionStore.getByTenant(tenantId);