Skip to main content

Tenancy & Row-Level Security

See tenant-context and route-wrapping for the canonical, citable rule statements. This page covers the mechanics behind them.

Every tenant-scoped table enforces isolation at the Postgres layer:

ALTER TABLE identity.organisations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON identity.organisations
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);
Never cast the GUC directly

current_setting('app.tenant_id', true)::uuid (without the NULLIF) is a landmine on pooled connections: once any transaction on a session has run set_config('app.tenant_id', …, true), the GUC's reset value is '' — defined-but-empty, not NULL — so the bare cast raises 22P02 and aborts every statement touching the table, because all policies on a table are evaluated even when another one would match. This took down the MCP token exchange in production (DIR-93). Use NULLIF(current_setting(…), '')::uuid, or a module helper like identity.row_in_current_tenant(tenant_id) which compares as text.

Tenant scoping happens in two distinct steps:

  1. Membership validation at the route layer. Every catalog and directory route uses the per-app authedRoute / authedRouteWithParams helpers (which compose withAuth + withTenantAuth). withTenantAuth resolves the active tenant (x-tenant-id header → jwt.tenant_id fallback) and verifies the user has an active membership for it. No SET LOCAL happens here.
  2. DB session scoping at the tools layer. When a tool opens a transaction via withTenantContext() from @constellation-platform/db, the helper executes SELECT set_config('app.tenant_id', $1, true) (transaction-local) so RLS picks up the value for every subsequent query in the transaction. The tenant-scoped Prisma client wraps this for you when you use createTenantClient(...).

Routes that read but never call into a tool can still be RLS-safe by going through createTenantClient(...), which does the transaction + SET LOCAL for each scoped query.

Route wrapping

The wrap is mandatory: see route-wrapping for the rule, the canonical wrap style, the escape-hatch syntax, and the CI enforcement script.