Skip to content

Day 2 — Tenant isolation, proven

Landed: 2026-07-02 · Verified: 10 isolation tests green in Postgres + Weaviate; Day-1 debts closed

Non-negotiable. The whole multi-customer promise rests on this day. Isolation is not assumed — it is attacked and proven.

Seal every customer’s data from every other, in the database and the search index. Prove it by attacking it, not by trusting the design on paper.

Plan
Built
Tested
What broke
The fix
Green
  1. PlanSeal every customer’s data from every other, in the database and the search index. Prove it by attacking it.
  2. BuiltForced database walls (Postgres RLS FORCED, an unprivileged role, one single access path — the tenant_txn seam) plus two separate Weaviate search compartments (multi-tenancy on, two tenants created explicitly, auto_tenant_creation off).
  3. TestedA sweep that finds every table on its own, then tries cross-customer reads and writes. 10 isolation tests, against real Postgres and Weaviate services, not mocks.
  4. What brokeThe reviews caught 2 real bugs: a wrong row count in one assertion, and a test that opened its database connection at the wrong moment.
  5. The fixSwitched to a true count instead of the wrong one, and moved the connection to the right point in the test. Written down, not papered over.
  6. Green10 isolation tests pass. Quick checks green (80 unit tests). 23 integration tests pass.
  • Postgres tenant isolation: Row-Level Security forced on tenant-scoped tables, an unprivileged application role that cannot bypass the walls, and exactly one access path into tenant data (the tenant_txn seam) that fails closed if the tenant is not set.
  • Weaviate tenant isolation: multi-tenancy enabled, two tenants created explicitly at bootstrap. auto_tenant_creation deliberately left off, so an unknown tenant is a hard failure, never a silently-created new shard.
  • A dynamic isolation sweep: 10 tests that find every tenant-scoped table on their own and attempt cross-tenant reads and writes against real Postgres and Weaviate, not fakes. A throwaway unprotected table was used to prove the sweep actually catches a table that forgets its wall — the guard itself was tested, not just trusted.
  • The rebuildable-from-Postgres design: the chunks table stores each piece’s searchable form (text, metadata, and the embedding vector) per tenant with RLS forced, so Weaviate is a derived index, not a second source of truth. If Weaviate is ever lost, a reindex job rebuilds it from Postgres with no customer re-upload and no re-embedding cost.
  • Day-1 debts closed as part of this day’s work (the widget_keys RLS gap flagged by Day 1’s security review).

Two real bugs were caught by review, not shipped silently:

  1. A wrong row count in one of the isolation assertions — fixed by switching to a true count.
  2. A test that touched the database at the wrong moment — a connection opened before the tenant context was set, which could have made a false-negative pass possible. Fixed by moving the connection to the correct point in the test.

Both are recorded here, not smoothed over, because a review that never finds anything on a day this security-sensitive would be a weaker signal than a review that found two real things and got them fixed.

What Day 2 added: tenant isolation

Postgres — RLS FORCED

documents + chunks tables walled per tenant; unprivileged role; one access path (tenant_txn), fails closed if unset

Weaviate — 2 tenants

multi-tenancy on, tenants created explicitly, auto_tenant_creation off — unknown tenant hard-fails

chunks table (rebuild source)

text + metadata + embedding vector per tenant — Weaviate can be rebuilt from here with no re-upload

Door A / B / C (Day 1)

tenant resolved once at the door, travels as data into this layer

Retrieval + agent (Days 5–6)

not yet built — will read through this same isolated layer

solid box = built this daydashed box = rest of the system (context, not new)
10 isolation tests: green (Postgres + Weaviate, real services)
Quick-gate: green (80 unit tests)
Integration: 23 tests pass
Cross-tenant leaks: 0
Screenshot placeholder — reserved for a future GitHub Actions run screenshot of the isolation sweep, if the owner wants one alongside the pasted output above.