@samveys/tenancy

Version
1.0.2Nov 5, 2026
Install
pnpm add @samveys/tenancy

From packages/tenancy/README.md in the framework repository.

Multi-tenancy that fails closed: code that needs a tenant and has none throws SVS_E114 (404); a write that names another tenant throws SVS_E115 (403).

Resolve the tenant per request

ts
import { createApp } from '@samveys/core'
import { http } from '@samveys/http'
import { header, path, subdomain, tenancy } from '@samveys/tenancy'

const tenants = new Map([['acme', { id: 'acme' }]])

export const app = createApp({
  name: 'portal',
  plugins: [
    http(),
    tenancy({
      resolvers: [subdomain('app.example.com'), path('t'), header('x-tenant')],
      lookup: (id) => tenants.get(id),
    }),
  ],
})

Routes that need a tenant use the tenant middleware (use: ['tenant']) and read ctx.tenant. An unknown tenant is a 404. Use the header() resolver only behind a gateway that sets the header, because clients can send any value.

Keep tenants apart

LayerHelperWhat it does
ORM, one databasetenantScoped(Model, 'tenantId')Every query filters on the tenant column; create() sets it and refuses another tenant's id
Cache, lockstenantCache(cache)Prefixes every key, lock and funnel name with t:<id>:; flush() is refused
WorkersrunAsTenant(id, lookup, work)Looks the tenant up again before running a job; refuses unknown tenants and tenant switches
Database per tenanttenantDatabases(connection)One connection per tenant, created on first use
PostgreSQLenableRowLevelSecurity(db, table) + withTenantSession(db, work)Row level security on current_setting('app.tenant_id'), set per transaction with SET LOCAL

Row level security does not apply to superusers or, without FORCE, to the table owner; enableRowLevelSecurity forces it, and the app should connect as a plain role.