---
title: "Tenancy"
description: "Tenant identity, requireTenantId, shared database vs database-per-tenant pooling."
---

> Documentation Index
> Fetch the complete documentation index at: https://logbun-docs.abshahin.workers.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tenancy

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:

```text
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:

```text
database_per_tenant mode requires LogbunConfig.adapterFactory
```

```ts
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 "…"`.

Source: https://logbun-docs.abshahin.workers.dev/concepts/tenancy/index.mdx
