The two-book model
One codebase, two databases, one per legal entity.
Why not one database
- Separate regulators (SCA vs IFSCA). Audit trails cleanly separable at handover time.
- Separate custodians — different reconciliation partners.
- Separate tax regimes.
- Separate risk profiles — a data-loss incident on India shouldn't touch UAE.
What's shared
- The FastAPI app — one image, one
main.py, one router set. - The
ledgerschema — every table + column, driven by one alembic history. - The posting engine —
services/posting.py, source-agnostic. - The recost dispatch —
apply_recost_gtnandapply_recost_viewtrade; routed byTransaction.source. - Every reconciliation check — India-only checks live in the same registry as
UAE checks and return
okwhen there's nothing to reconcile. - The frontend — one Vite build, book toggle in the sidebar.
What's per-book
| Per-book | UAE | India |
|---|---|---|
| Database | ledger_uae | ledger_ind |
| Backend port (local) | 8077 | 8078 |
| Env file | .env.uae.local | .env.ind.local |
ENTITY_ID | VALURA_UAE | VALURA_IND_IFSC |
| Chart of accounts seed | accounts.json (32) | accounts.india.json (19) |
| Sources | GTN, ZAG, Gold, Private | ViewTrade IFSC, GlomoPay LRS |
| India-only tables | (empty) | lrs_remittances, dividend_events |
| India-only endpoints | (return safe zeros) | 16 /v1/india/* endpoints |
The "degrade to OK on the wrong book" pattern
Every India check is registered on both books. When the check runs on UAE:
- Short-circuits on empty state — e.g.
lrs_viewtrade_tie_outquerieslrs_remittances, gets empty, returnsok. - Detects the wrong
ENTITY_ID— treasury / compliance checks readsettings.entity_idand return an early "not applicable" ok.
The "no UAE code changes" hard lock
classify.py— addedclassify_viewtradeandclassify_glomopayalongsideclassify_zag; existing functions untouched.recost_writer.py— addedapply_recost_viewtrade;apply_recost_gtnuntouched.posting.py— added the India-specific accounts toALL_POSTING_ACCOUNT_CODES; no UAE posting rule modified.config.py— added India settings; all UAE defaults preserved.
The migrations
One alembic history covers both books. Every migration MUST be a no-op on the book it doesn't concern (either targets shared tables, or targets tables that exist in both schemas but stay empty on the other book).
The frontend book toggle
The UI keeps a ledger_entity key in localStorage and reads it via
apiBase():
const ENTITY_BASE: Record<EntityId, string> = {
UAE: import.meta.env.VITE_LEDGER_API_URL || "http://localhost:8077",
IND: import.meta.env.VITE_LEDGER_API_URL_IND || "http://localhost:8078",
}
Toggling calls window.location.reload() — clears react-query caches so no
stale UAE data leaks into an India view.
Adding a third book
- Add
accounts.<xx>.jsontodb/seeds/. - Add
ENTITY_ID=VALURA_<XX>support to the seeder + config. - Stand up a third DB + backend + env file.
- Add per-book sources (adapters + classify + importer + sync).
- Add per-book endpoints with the same "degrade to OK on other books" pattern.
No shared-code fork.