@venturekit-pro/tenancy
Installation
Section titled “Installation”npm install @venturekit-pro/tenancyWhat It Provides
Section titled “What It Provides”Tenant Context
Section titled “Tenant Context”import { TenantContext, createTenantContext, getCurrentTenant, resolveTenant } from '@venturekit-pro/tenancy';
const tenant = getCurrentTenant(ctx);// { id: 'acme', slug: 'acme', metadata: { ... } }Middleware
Section titled “Middleware”import { createTenantMiddleware, createQuotaMiddleware } from '@venturekit-pro/tenancy';import type { IsolationConfig } from '@venturekit-pro/tenancy';
// `strategy` selects the **data** isolation model (shared tables vs schema vs// database). `resolution` selects how the tenant is identified per request.const isolation: IsolationConfig = { strategy: 'shared', resolution: 'subdomain' };
// `createTenantMiddleware()` requires `config` (resolution + isolation rules)// and `lookupTenant` (your tenant store). `createQuotaMiddleware()` requires// `quotaKey` and `checkUsage` (returns current usage for the quota).middleware: [ createTenantMiddleware({ config: isolation, lookupTenant: async (id) => loadTenant(id), }), createQuotaMiddleware({ quotaKey: 'apiRequests', checkUsage: async (tenantId) => getMonthlyUsage(tenantId), }),]Error Types
Section titled “Error Types”import { TenantNotFoundError, TenantSuspendedError, TenantInactiveError, QuotaExceededError } from '@venturekit-pro/tenancy';Quota Checking
Section titled “Quota Checking”import { checkQuotas } from '@venturekit-pro/tenancy';
// checkQuotas(tenantId, quotas, checkUsage)// quotas: TenantQuotas — a map of quotaKey → numeric limit// checkUsage: (tenantId, quotaKey) => Promise<number> — current usageawait checkQuotas( tenantId, { apiRequests: 10_000, storage: 5_000_000_000 }, async (id, quotaKey) => loadCurrentUsage(id, quotaKey),);Resolution Strategies
Section titled “Resolution Strategies”| Strategy | Source |
|---|---|
subdomain | acme.app.example.com |
custom_domain | app.acme.com (domain lookup via domainLookup) |
path | /t/acme/api/tasks |
header | X-Tenant-ID: acme (configurable via headerName) |
query | ?tenant=acme (configurable via queryParam) |
jwt | tenant_id claim (configurable via jwtClaim) |
custom | pluggable async resolver via customResolver |
For handler() stacks prefer createRuntimeTenantMiddleware({ resolver }) — it populates ctx.tenant — with hostPrefixDomainResolver() for the api.<tenant-domain> convention. See the API reference.
Per-tenant Roles & Scopes
Section titled “Per-tenant Roles & Scopes”import { createTenantUserScopesMiddleware, packTenantRoles } from '@venturekit-pro/tenancy';import { createRoleScopesResolver } from '@venturekit/auth';
// Grants the user's role-for-this-tenant scopes onto ctx.user.scopes — claims first// (packed custom:tenantRoles), then the tenant-user store.const tenantUserScopes = createTenantUserScopesMiddleware({ tenantUser: (ctx) => loadMembership(ctx.tenant!.id, ctx.user!.id), // store fallback scopesByRole: createRoleScopesResolver({ querier: query }).lookup, // or a static Record<role, scopes[]>});
// Write path: keep the packed claim current on every membership changeconst packed = packTenantRoles(memberships.map((m) => ({ tenantId: m.tenantId, role: m.role })));await adminUpdateUserAttributes({ username, attributes: { 'custom:tenantRoles': packed } });Declare tenancy: true on the auth intent so the pool carries custom:tenantId + custom:tenantRoles.
Row-level Security & Handoff
Section titled “Row-level Security & Handoff”createTenantRlsMiddleware()— opens@venturekit/data’srunWithTenant()scope for the resolved tenant so Postgres RLS filters every query. Place it right after the tenancy middleware.createTenantHandoffAuthorize({ resolveTenantByHost, isActiveTenantUser })— the membership gate for@venturekit/auth/server’s cross-domain session handoff (white-label custom domains).
Tenant CRUD & Lifecycle
Section titled “Tenant CRUD & Lifecycle”import { createTenant, getTenantBySlug, suspendTenant, restoreTenant, hardDeleteTenant } from '@venturekit-pro/tenancy';import { query } from '@venturekit/data';
const tenant = await createTenant(query, { slug: 'acme', displayName: 'Acme', primaryDomain: 'acme.io' });await suspendTenant(query, { tenantId: tenant.id, reason: 'unpaid', audit: (e) => record(query, toAuditEvent(e)) });await restoreTenant(query, { tenantId: tenant.id });await hardDeleteTenant(adminQuery, { tenantId: tenant.id }); // introspects every tenant_id table, deletes leaves-firstThe vk_tenants table ships with the package’s migration (0000_vk_tenancy_tenants.sql), auto-applied by vk migrate; extra columns you ALTER on ride along through the same helpers.
Per-user Erasure & Export (GDPR)
Section titled “Per-user Erasure & Export (GDPR)”The same introspecting walker, keyed on the column that marks a row as a user’s (user_id by default):
import { exportUserData, executeUserErasure, planUserErasure } from '@venturekit-pro/tenancy';import { withTransaction } from '@venturekit/data';
const bundle = await exportUserData(query, { userId, skipTables: ['audit_log'] }); // art. 20 — { tables: { 'public.posts': rows[] }, … }const plan = await planUserErasure(query); // preview: tables in deletion orderawait withTransaction((tx) => executeUserErasure(tx.query, { userId, skipTables: ['audit_log'] })); // art. 17, all-or-nothingawait adminDeleteUser(userId); // the Cognito account is separateskipTables names the tables to leave alone — an append-only ledger you must keep, which you anonymise instead. Rows keyed by another column (author_id) take one more call with column.
Dependencies
Section titled “Dependencies”@venturekit/core— required@venturekit/runtime— runtime middleware@venturekit/data— optional; supplies theQuerierfor CRUD / lifecycle and the RLS scope
Related
Section titled “Related”- Multi-Tenancy Guide — setup walkthrough
- Middleware — composing with other middleware
- WebSockets — tenant-scoped messaging
- API Reference — full type documentation