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.
68 lines
3.4 KiB
Markdown
68 lines
3.4 KiB
Markdown
# RAMZ Tenant Isolation — Decision
|
|
|
|
**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
|
|
|
|
## 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:
|
|
|
|
1. **Separate database per tenant** — strongest isolation, highest ops overhead.
|
|
2. **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.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
|
|
|
|
- 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-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 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 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.
|