Docs/enterprisev1.0.0-rc.1

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.

From the canonical UDIOT public documentation.Try it in the playground ↗