Multi-tenancy
Use @tagna/udiot/platform for tenant resolution and scoped services.
Public exports include HeaderTenantResolver, JwtClaimTenantResolver,
SubdomainTenantResolver, tenantResolverMiddleware, TenantScopeManager,
createTenantScopedContainer, runWithTenantScope, resolveTenantContext
and TENANT_CONTEXT_TOKEN. The internal TenantScoped decorator is not
exported by the npm package.
Tenant-owned service registration
import assert from 'node:assert/strict';
import {
TenantScopeManager,
type TenantContext,
} from '@tagna/udiot/platform';
const manager = new TenantScopeManager();
const token = Symbol('tenant-settings');
manager.register(token, tenant => ({ tenantId: tenant.id }));
const tenant: TenantContext = { id: 'acme', slug: 'acme' };
const settings = manager.resolve<{ tenantId: string }>(token, tenant);
assert.equal(settings.tenantId, 'acme');
manager.disposeTenant(tenant.id);
TenantScopeManager caches factory results by tenant ID. Dispose entries when
appropriate; dropping the cache is not the same as closing application-owned
database/network resources. Do not put per-user request data in tenant-wide
singletons.
Security boundary
Tenant selection is routing, not authorization. Authenticate first and use verified claims for identity. Do not trust a raw header or host as proof that a caller may access a tenant. Reject unresolved production tenants; do not fall back to a shared database. Include tenant and host variants in cache keys and test cross-tenant access failures.
tenantResolverMiddleware() enforces part of this. A verified tenant_id
claim always wins. When a signed-in user without a claim selects a tenant by
subdomain or header, the request is rejected with 403 unless an
authorizeTenant callback confirms membership. Anonymous requests may still
select a tenant (for public tenant sites). Pass resolvers to choose which
strategies run and in what order.
app.use(tenantResolverMiddleware({
authorizeTenant: async (user, tenantId) => memberships.has((user as { id: string }).id, tenantId),
}));
Use per-request child containers via server request scope.
createTenantScopedContainer binds the tenant context to a child container;
register consumer-owned scoped providers explicitly. Parent singletons retain
their parent lifetime.
See deployment and support levels.