From 4961a07356b46afb453ea7cf3485b637bd4e5c49 Mon Sep 17 00:00:00 2001 From: Wmj Ismail Date: Tue, 18 Aug 2026 15:55:17 +0800 Subject: [PATCH] docs: revise RAMZ isolation decision to database-per-tenant Schema-per-tenant was the original call, but falah-os-master's infrastructure/postgres/init.sql already establishes database-per-service as the platform convention (falahid, falahwallet, falahramz, falahmock, falahistore). Database-per-tenant matches that pattern and is a stronger isolation boundary than schemas would have been. --- .../ramz-tenant-isolation-decision.md | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/architecture/ramz-tenant-isolation-decision.md b/docs/architecture/ramz-tenant-isolation-decision.md index 430e2ad..224773f 100644 --- a/docs/architecture/ramz-tenant-isolation-decision.md +++ b/docs/architecture/ramz-tenant-isolation-decision.md @@ -1,6 +1,6 @@ # RAMZ Tenant Isolation — Decision -**Status:** Resolved +**Status:** Resolved (revised 2026-08-18 — see amendment below) **Blocks:** Phase 2 of the [moslem04 Sync API spec](./sync-api-spec.html) (admin console read view) **Originally flagged in:** Nur Falah Admin Console PRD review @@ -17,10 +17,19 @@ Two options were on the table: ## Decision -**Schema-per-tenant on a single shared Postgres cluster.** +**Database-per-tenant on the shared Postgres instance.** -Each tenant (EstateOS, Tanah Hidup, Cair) gets its own Postgres schema. Not a separate -server, not a shared table filtered by a `tenant_id` column. +Each tenant (EstateOS, Tanah Hidup, Cair) gets its own Postgres *database* — `falahestateos`, +`falahtanahhidup`, `falahcair` — not a schema, not a shared table filtered by a `tenant_id` +column. + +> **Amendment (2026-08-18):** this was originally decided as schema-per-tenant. Inspecting +> `falah-os-master/infrastructure/postgres/init.sql` showed the codebase already has an +> established convention — **database-per-service** (`falahid`, `falahwallet`, `falahramz`, +> `falahmock`, `falahistore`, all separate databases in one Postgres instance, not schemas). +> Database-per-tenant is a stronger isolation boundary than schema-per-tenant anyway, and it +> keeps one consistent pattern across the platform instead of introducing a second, competing +> one. Revised to match. ## Why @@ -31,25 +40,25 @@ server, not a shared table filtered by a `tenant_id` column. boundary is something that can actually be demonstrated. - This was flagged as a trust/compliance blocker for the UK Innovator Visa pitch — the isolation model isn't just an engineering choice, it's part of the pitch. -- Fully separate databases (or separate servers) solve the same trust problem but multiply - ops burden — separate backup policies, connection pools, migrations to run three times - instead of once. Not justified at current scale. -- Schema-per-tenant keeps one cluster, one backup policy, one connection pool — while still - giving each tenant a real, auditable namespace boundary. +- Separate databases on one Postgres instance is the platform's existing convention, not a + new ops burden — `falah-os-master` already runs 5 databases this way. Adding 3 more + follows the grain of the system instead of cutting against it. ## What RAMZ still does -RAMZ doesn't disappear — it moves down a level. Within each tenant's schema, RAMZ handles -the finer-grained access control that a schema boundary can't: which user or law firm +RAMZ doesn't disappear — it moves down a level. Within each tenant's database, RAMZ handles +the finer-grained access control that a database boundary can't: which user or law firm within EstateOS can see which case, which land officer within Tanah Hidup can see which lease. RAMZ was never going to be strong enough to be the *only* thing standing between tenants — now it doesn't have to be. ## Consequence for the sync API -The `tenant_id` field in the [sync spec](./sync-api-spec.html) becomes a schema selector, -not a row filter. The admin console's `/v1/admin/metadata` endpoint queries across schemas -explicitly, per-tenant — there is no accidental cross-tenant query path. +The `tenant_id` field in the [sync spec](./sync-api-spec.html) becomes a database selector +(one connection pool per tenant), not a row filter. The `moslem-admin` service's +`/v1/admin/metadata` endpoint holds one Postgres pool per tenant database — there is no +shared table and no accidental cross-tenant query path, because a query issued against one +pool cannot physically reach another tenant's rows. ## Still open