Skip to content

Multi-Tenant & RLS

Local Lead Legend enforces tenant isolation at two layers: the application query layer and the database (Postgres Row-Level Security). This is defense in depth — and the isolation boundary once multiple agencies share the same database.

Every request resolves an acting org (see Subaccounts & X-Org-Id), and every query is scoped to that org. A route that forgets org_id is the number-one bug class in Local Lead Legend, so org scoping is checked first in review.

Membership is validated against crm.org_members: a caller who is not a member of the requested org gets 403, and the API never leaks whether the org exists.

Layer 2 — database Row-Level Security (RLS)

Section titled “Layer 2 — database Row-Level Security (RLS)”

At the database level, RLS is enabled and forced on the crm base tables that carry an org_id, with an org_isolation policy on each. The acting org is bound to the database session on every request via a per-request setting (a Postgres GUC, app.current_org_id), and the policy scopes every query to the acting org and its direct children (honoring the parent_org_id hierarchy). The application connects as a non-superuser role that cannot bypass RLS.

This means that even if an application-layer query forgot to scope by org_id, the database would still return only the acting org’s rows. RLS is fail-closed.

Resolving the acting org itself has to query membership before the org GUC can be set (a chicken-and-egg problem), and a brand-new user’s first request provisions their owner membership. Those steps run under a sanctioned system bypass for resolution and owner-provisioning only, then the request immediately switches to the org-scoped policy for the rest of the handler. The bypass never leaks past resolution.

Legitimately cross-org paths — provider webhooks that carry no X-Org-Id, and some operator/settings reads — take the sanctioned system bypass deliberately, and resolve the org internally. Everything else is org-scoped.

DB-level isolation is the binding gate for letting multiple agencies share one database safely. With RLS forced and the app on a non-bypass role, a child-only token is blocked from the agency org at the database itself — not just by application code.