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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# RAMZ Tenant Isolation — Decision
|
# 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)
|
**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
|
**Originally flagged in:** Nur Falah Admin Console PRD review
|
||||||
|
|
||||||
@@ -17,10 +17,19 @@ Two options were on the table:
|
|||||||
|
|
||||||
## Decision
|
## 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
|
Each tenant (EstateOS, Tanah Hidup, Cair) gets its own Postgres *database* — `falahestateos`,
|
||||||
server, not a shared table filtered by a `tenant_id` column.
|
`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
|
## Why
|
||||||
|
|
||||||
@@ -31,25 +40,25 @@ server, not a shared table filtered by a `tenant_id` column.
|
|||||||
boundary is something that can actually be demonstrated.
|
boundary is something that can actually be demonstrated.
|
||||||
- This was flagged as a trust/compliance blocker for the UK Innovator Visa pitch — the
|
- 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.
|
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
|
- Separate databases on one Postgres instance is the platform's existing convention, not a
|
||||||
ops burden — separate backup policies, connection pools, migrations to run three times
|
new ops burden — `falah-os-master` already runs 5 databases this way. Adding 3 more
|
||||||
instead of once. Not justified at current scale.
|
follows the grain of the system instead of cutting against it.
|
||||||
- Schema-per-tenant keeps one cluster, one backup policy, one connection pool — while still
|
|
||||||
giving each tenant a real, auditable namespace boundary.
|
|
||||||
|
|
||||||
## What RAMZ still does
|
## What RAMZ still does
|
||||||
|
|
||||||
RAMZ doesn't disappear — it moves down a level. Within each tenant's schema, RAMZ handles
|
RAMZ doesn't disappear — it moves down a level. Within each tenant's database, RAMZ handles
|
||||||
the finer-grained access control that a schema boundary can't: which user or law firm
|
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
|
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
|
lease. RAMZ was never going to be strong enough to be the *only* thing standing between
|
||||||
tenants — now it doesn't have to be.
|
tenants — now it doesn't have to be.
|
||||||
|
|
||||||
## Consequence for the sync API
|
## Consequence for the sync API
|
||||||
|
|
||||||
The `tenant_id` field in the [sync spec](./sync-api-spec.html) becomes a schema selector,
|
The `tenant_id` field in the [sync spec](./sync-api-spec.html) becomes a database selector
|
||||||
not a row filter. The admin console's `/v1/admin/metadata` endpoint queries across schemas
|
(one connection pool per tenant), not a row filter. The `moslem-admin` service's
|
||||||
explicitly, per-tenant — there is no accidental cross-tenant query path.
|
`/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
|
## Still open
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user