Skip to content

Tenancy

Tenant identity, requireTenantId, shared database vs database-per-tenant pooling.

Updated View as Markdown

Every log may carry tenantId. Routing, integrity tips, RAM queues, and (in database_per_tenant) destination connections key off that field.

isTenantIdPresent treats empty and whitespace-only strings as missing.

requireTenantId

Default false (single-tenant / back-compat). When false and not database_per_tenant, construction emits limit / unsafe_default_require_tenant.

When true, fire / fireAsync / query require a present tenantId. fire emits drop / require_tenant_id; fireAsync and query throw:

tenantId is required when requireTenantId is true

tenancy.mode === 'database_per_tenant' forces this on.

tenantId is never truncated. Oversize ids (maxTenantIdBytes, default 256 UTF-8 bytes) are dropped (fire) or rejected (fireAsync) — truncation would merge tenants.

ENTERPRISE_DEFAULTS sets { mode: 'durable', requireTenantId: true }.

Plugins may fill tenantId via getTenantId only when the caller omitted it. Resolve from an authenticated session or verified JWT. Never use a raw x-tenant-id header unless the gateway overwrites that header.

single_database

Default when tenancy is omitted. One destination adapter stores every tenant (typically a tenant_id column). query({ tenantId }) filters that adapter. Omitting tenantId (and not requiring it) queries unscoped (tenantId === null).

database_per_tenant

Each tenant gets its own adapter, created by adapterFactory from resolveConnection config. adapterFactory is required — the pool never does new adapter.constructor(...). Missing factory throws in the constructor:

database_per_tenant mode requires LogbunConfig.adapterFactory
const audit = new AuditLogger({
  namespace: 'api',
  mode: 'durable',
  reliability,
  adapter: new TursoAdapter({
    url: process.env.TURSO_URL!,
    authToken: process.env.TURSO_TOKEN!,
  }),
  tenancy: {
    mode: 'database_per_tenant',
    resolveConnection: async (tenantId) => ({
      url: `libsql://audit-${tenantId}.turso.io`,
      authToken: process.env.TURSO_TOKEN!,
    }),
    knownTenantIds: () => listTenantIdsFromControlPlane(),
  },
  adapterFactory: (config) =>
    new TursoAdapter({
      url: String(config.url),
      authToken: String(config.authToken),
    }),
  pool: { maxActiveConnections: 50 },
  requireTenantId: true,
});

pool.maxActiveConnections defaults to 50. Exhaustion throws pool_exhausted when every entry is pinned.

adapter.init() must be idempotent: the pool always calls it after adapterFactory.

Retention prune in this mode visits pool.listActiveTenantIds() plus tenancy.knownTenantIds() only. It does not prune the constructor adapter (the base adapter is not a tenant database). Provide knownTenantIds so cold tenants still get pruned.

ClickHouse is designed as a shared warehouse (PARTITION BY month, tenant_id column), not one database per tenant.

Query tenant semantics

On IAdapter.query:

tenantId Meaning
null Unscoped (no tenant filter)
'' Tenant-less / __global__ chain only (tenant_id IS NULL OR tenant_id = '')
any other string That tenant

Integrity restore uses '' for logs without a tenant. AuditLogger.query with omitted/undefined tenantId passes null unless requireTenantId is on. In database_per_tenant, a missing tenant throws; a present tenant pins the pooled adapter. Adapter query failures after a successful pin are not wrapped. Pool resolve / pin failures throw Failed to resolve tenant adapter for tenantId "…".

Navigation

Type to search…

↑↓ navigate↵ selectEsc close