> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-002 — Multi-Tenancy: Shared Schema, RLS, Cell-Ready

> Status: Proposed (RECONSTRUCTION — requires validation) · Date: 2026-08-17 (reconstructed) Refs: ../constitution/founding-constitution.md.

Status: **Proposed (RECONSTRUCTION — requires validation)** · Date: 2026-08-17 (reconstructed)
Refs: [`../constitution/founding-constitution.md`](/constitution/founding-constitution)

> ⚠️ Reconstructed from the material we have, not recovered. Tags: **\[derived]** is supported by
> evidence in the repository · **\[inferred]** is deduced · **\[proposed]** is a gap I filled.

## Context

Three shapes were available: a database per tenant, a schema per tenant, or a shared schema with
row-level isolation. The first two isolate perfectly and stop scaling at a few hundred tenants —
migrations, connection pools and backups all multiply. The third scales and puts the entire
isolation burden on getting one thing right, every time. **\[inferred]**

## Decision

**Shared schema with `tenant_id` on every tenant-scoped table, isolated by row-level security.**
\[derived]

RLS is **the boundary, not a second layer** behind application checks. \[derived] An application bug
must not be able to cross a tenant, and that is only true if the database refuses.

Database access goes only through `getDb(tenantCtx)`. \[derived] There is no path to a connection
without a tenant context.

Every tenant-scoped table also carries **`cell_id`**, present from the first migration and unused.
\[derived] It is the seam for regional partitioning: when a tenant must be pinned to a region, the
column is already there and populated.

Every uniqueness constraint on tenant-scoped data includes `tenant_id`. \[derived — added 2026-08-17]

## Consequences

* One migration, one pool, one backup, for any number of tenants.
* Regional partitioning becomes a routing problem rather than a migration.
  − Every table needs RLS and every RLS policy needs a denial test. A forgotten policy is a breach.
  − Noisy-neighbour effects are real and are handled by rate limiting and quotas, not by isolation.

## What I could not determine

Whether `cell_id` was intended as a physical region identifier or a logical shard key. **\[inferred]**
— I have treated it as a region, which is how the data-residency material reads.
