/isolation/ · Design document

One database per tenant. Not one policy per table.

Multi-tenant isolation is the decision that is hardest to reverse. This page states the alternative accurately, then explains why Harness puts the boundary at the database.

§1

Row Level Security, stated fairly

Postgres Row Level Security attaches policies to a table. Once enabled, every row a query returns or modifies must satisfy a policy expression, evaluated per row with the caller's role and session settings. Supabase builds on this: the client sends a JWT, Postgres exposes its claims through auth.uid() and auth.jwt(), and policies filter shared tables by tenant or user. See Supabase's RLS guide.

It works, and it has real advantages: one schema to migrate, one connection pool, cross-tenant analytics in a single query, and a mature Postgres you can use this afternoon. Enabling it is one statement. Many production systems run on it well.

The costs are structural. Authorization lives in SQL policies spread across every table, so correctness is the conjunction of all of them. A policy that is missing, or that references the wrong column, does not fail loudly; it returns other tenants' rows. Every new table must be remembered. Performance depends on the planner inlining the policy expression. And the blast radius of a mistake is the whole table, which is every tenant.

§2

The database boundary

Every tenant gets its own database. Never schema-per-tenant, never shared tables, no search_path switching, no RLS. On Cloudflare this is not something Harness built; it is the structural default. One Worker, one D1 database, per backend. A module cannot reach another tenant's data because no handle to it exists in the process.

acmeD1
tables
_migrations
_secrets (envelope)
_audit (hash chain)
northwindD1
tables
_migrations
_secrets (envelope)
_audit (hash chain)
globexD1
tables
_migrations
_secrets (envelope)
_audit (hash chain)
tenant ND1
tables
_migrations
_secrets (envelope)
_audit (hash chain)

No arrow from any module to a control database. Migration history, secrets and audit log live inside the tenant database, so backup, restore, move and offboarding are one-database operations with no blast radius.

§3 Designed

Two-tier secrets, envelope encryption

Specified in #23 and #24. Unbuilt. Written in the conditional on purpose.

Tier one would be Worker secrets: the handful of values the runtime needs before it can open the database, such as the D1 binding and the key-encryption key. Tier two would be application secrets stored inside the tenant database, each encrypted with a data key that is itself wrapped by the tier-one key. Rotating the tier-one key re-wraps data keys without touching ciphertext.

A Kms trait would sit behind the wrapping so the same modules run against Cloudflare secrets, a cloud KMS, or a file on a developer laptop. The audit log would be append-only and hash-chained so tampering is detectable. The design maps onto SOC 2 control families; it is not a compliance claim, and none is made.

Isolation you do not have to configure is isolation you cannot misconfigure.