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.
3.4 KiB
RAMZ Tenant Isolation — Decision
Status: Resolved (revised 2026-08-18 — see amendment below) Blocks: Phase 2 of the moslem04 Sync API spec (admin console read view) Originally flagged in: Nur Falah Admin Console PRD review
The question
Falah OS runs one shared backend serving three tenants — EstateOS (law firm estate/probate, Syariah Court), Tanah Hidup (waqf land lease, JKPTG), and Cair (govt case routing). How is each tenant's data isolated from the others?
Two options were on the table:
- Separate database per tenant — strongest isolation, highest ops overhead.
- Row-level RAMZ-gated access in a shared schema — cheapest, weakest isolation story.
Decision
Database-per-tenant on the shared Postgres instance.
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.sqlshowed 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
- The three tenants aren't multiple firms sharing one product (the Clio model) — they're
three structurally different products, each answering to a different regulator
(Syariah Court, JKPTG, government agencies). A
WHERE tenant_id = ?filter on a shared table is not an answer a regulator's auditor will accept at face value; a schema 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.
- Separate databases on one Postgres instance is the platform's existing convention, not a
new ops burden —
falah-os-masteralready 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 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 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
This resolves the isolation model. It does not resolve whether Core Ledger already emits the billing-trigger events Phase 2 needs, or who owns panel-lawyer vetting — both remain open from the earlier PRD review.