Skip to main content

Target Platform Architecture

Status: Forward-looking blueprint. This document describes where DemozPay is going, not only what exists today. Every service and infrastructure item is tagged with a maturity label so "now" is never confused with "later."

Maturity legend:

  • 🟒 Today β€” exists in code now
  • πŸ”΅ MVP β€” needed to launch
  • 🟑 Post-MVP β€” first customers / growth
  • βšͺ Long-term β€” scale / future products

Operating model (read first): DemozPay is a payroll-first fintech platform. Payroll builds the trust and data that unlock financial products. Partner banks custody all funds; DemozPay never holds customer money. The double-entry Ledger is DemozPay's internal record of truth, reconciled against partner-bank statements. There is no internal wallet holding value (ADR-014 β€” orchestrator, not custodian).


1. Executive Summary​

DemozPay's thesis β€” payroll creates trust; trust unlocks financial products; banks custody, DemozPay orchestrates β€” dictates the architecture: a federation of independently-replaceable services around a single money-truth ledger, with payroll as the first product and more products reusing the same money, identity, and compliance rails.

We are pre-launch and deliberately launch-first: ship payroll on a small, comprehensible footprint; keep every service boundary expressed as a stable contract from day one; extract services into independent deployments only when load, team size, or language needs justify it. The contract is permanent; the deployment topology evolves.

Three decisions anchor the design:

  1. Contracts are the permanent architecture; deployments are not. Every boundary has a gRPC proto (sync) and a registered event schema (async) now, even while services share one deployable. This is what lets Payroll move NestJS β†’ Go later with zero impact on consumers.
  2. Partner banks custody; the Ledger records truth. No internal wallet. The double-entry Ledger is DemozPay's authoritative record of what moved; it is reconciled against partner-bank statements, which are the custody ground truth.
  3. One money core, many products. Ledger + Bank Gateway are the shared rails. Payroll today; EWA, Lending, Equb, and later Savings/Merchant/Cards reuse the same rails without platform redesign.

What launches (πŸ”΅ MVP): a thin Gateway/BFF, Identity, Tenancy, Workforce, Payroll (NestJS), KYC/Screening, the Ledger (Go) and Bank Gateway (Go) actually deployed with a bank-sandbox adapter, in-process Notifications, and Kafka carrying real cross-context events. Everything else (Payments-Orchestrator, Reconciliation-as-a-service, Treasury, Fraud, more products) is contract-ready but added post-MVP.


2. Target Architecture Diagram​

Labels show what exists when. Solid = MVP; dashed = later.

employer-web 🟒 admin-web 🟒 employee-web 🟒 (fi/merchant-web βšͺ frozen until their product launches)
β”‚ β”‚ β”‚
└────── HTTPS/REST β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ API GATEWAY / BFF πŸ”΅ β”‚ thin: auth, routing, rate-limit,
β”‚ (NestJS at MVP) β”‚ idempotency-key, API versioning
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ gRPC (sync, request path)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό β–Ό β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚IDENTITYβ”‚ β”‚ TENANCY β”‚ β”‚ WORKFORCEβ”‚ β”‚ COMPLIANCE β”‚ β”‚ PAYROLL πŸŸ’πŸ”΅ β”‚
β”‚& Accessβ”‚ β”‚ /Org β”‚ β”‚(employee)β”‚ β”‚ KYC+Screen β”‚ β”‚ (NestJS β†’ Go βšͺ) β”‚
β”‚πŸ”΅ NestJSβ”‚ β”‚πŸ”΅ NestJSβ”‚ β”‚πŸ”΅ NestJS β”‚ β”‚πŸ”΅ NestJS β”‚ β”‚ behind gRPC proto β”‚
β”‚ β†’ Go βšͺ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ gRPC (KYC gate) β”‚ gRPC (post)
β”‚ β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ MONEY CORE β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚
β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ LEDGER πŸŸ’πŸ”΅ β”‚ β”‚ record of truth
β”‚ PAYMENTS β”‚ β”‚ BANK GATEWAY β”‚ β”‚ β”‚ (Go) β”‚ β”‚ (NOT custody)
β”‚ ORCHESTRATOR│─▢│ πŸŸ’πŸ”΅ (Go) │──────┼─▢│ double-entry, β”‚ β”‚
β”‚ 🟑 light β”‚ β”‚ abstraction β”‚ β”‚ β”‚ immutable β”‚ β”‚
β”‚ (no Temporalβ”‚ β”‚ over partner β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ at MVP) β”‚ β”‚ FIs β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ adapter interface (same port)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ bank-sandbox β”‚ πŸŸ’πŸ”΅ β”‚ Dashen / CBE β”‚ 🟑βšͺ β”‚ Telebirr / β”‚ βšͺ
β”‚ (dev adapter) β”‚ (real adapters) β”‚ EthSwitch β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↑ partner banks HOLD the funds; Bank Gateway moves money via their APIs ↑

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ KAFKA / REDPANDA πŸ”΅ (events + schema registry) ─────────────────┐
β”‚ demoz.payroll.run.approved.v1 Β· demoz.ledger.entry.posted.v1 Β· β”‚
β”‚ demoz.disbursement.settled.v1 Β· demoz.kyc.approved.v1 β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–Ό πŸ”΅ β–Ό 🟑 / βšͺ
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚NOTIFICATIONS β”‚ πŸ”΅ in-process (TS) β†’ 🟑 service β”‚ RECONCILIATION 🟑 Β· FRAUD βšͺ Β· β”‚
β”‚ SMS β”‚ β”‚ TREASURY βšͺ Β· PRODUCTS: EWA/Lendingβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ /Equb 🟒(flag) Β· Savings/Cards βšͺ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Platform: Postgres πŸ”΅ Β· Outbox πŸ”΅ Β· OTel/Prometheus/Grafana 🟑 Β· Vault 🟑 Β· K8s 🟑 Β· Mesh βšͺ

3. Service Map (maturity + language path)​

ServiceMaturityLanguage pathWhy it exists
API Gateway / BFFπŸ”΅ MVPNestJS β†’ (Go/Envoy βšͺ)Single front door; decouples frontends from internal topology
Identity & AccessπŸ”΅ MVPNestJS β†’ Go optional βšͺAuth/sessions/MFA; the trust spine
Tenancy / OrgπŸ”΅ MVPNestJSOrgs (business/FI/merchant), members, roles, SoD
Workforce (Employee)πŸ”΅ MVPNestJSEmployee/contract data β€” the trust layer
Payroll🟒 today, πŸ”΅ MVPNestJS β†’ Go βšͺ behind gRPCFirst product; Go migration only after prod validation
Ledger🟒 built, πŸ”΅ deploy at MVPGoRecord of money truth (not custody); double-entry, immutable
Bank Gateway🟒 built, πŸ”΅ deploy at MVPGoAbstraction over partner FIs; bank-sandbox now, real banks later
KYC / Screening🟒 today, πŸ”΅ MVPNestJS β†’ Go βšͺ (screening)Gate every product; AML/sanctions
NotificationsπŸ”΅ in-processTS in-proc β†’ Go service 🟑SMS at launch; extract when fan-out grows
Payments Orchestrator🟑 Post-MVPGo, lightweight β†’ Temporal βšͺSaga coordination once flows span multiple services
Reconciliation🟑 Post-MVPGoDaily Ledger ↔ bank-statement matching as a standalone job
EWA / Lending / Equb🟒 built, flag-gatedNestJSLaunch each when the product launches
Treasuryβšͺ Long-termGoLiquidity across multiple partner banks
Fraud / Riskβšͺ Long-termGoReal-time scoring once volume warrants
Walletβšͺ future product (not a dependency)β€”Only if DemozPay ever offers stored value; not the operating model today
Savings / Merchant / QR / Bill / Cards / FX / Remittanceβšͺ Long-termGo/TSNew products on the stable rails

4. Bounded Context Map (custody-correct)​

TRUST/IDENTITY πŸ”΅ MONEY πŸ”΅ COMPLIANCE πŸ”΅ PRODUCT
IdentityΒ·Tenancy β”Œβ”€ Ledger (record of truth) ─┐ KYCΒ·Screening Payroll πŸ”΅
Β·Workforce β”‚ Bank Gateway (movement β”‚ Β·Fraud βšͺ EWA/Lending/
β”‚ via partner FIs) β”‚ Β·Audit Equb 🟒(flag)
β”‚ Payments-Orchestrator 🟑 β”‚ Savings/Cards βšͺ
β”‚ Treasury βšͺ β”‚
└─ Partner banks CUSTODY funds β”˜

Custody statement (the rule that prevents drift): customer funds live in partner financial institutions, never in DemozPay. The Ledger is DemozPay's internal double-entry record of what should be and what moved; Reconciliation continuously proves the Ledger against partner-bank statements, which are the custody ground truth. There is no internal wallet holding value. (The wallet:member:… identifiers in the Equb code are ledger read-model keys, not stored balances β€” ADR-014.)

Aggregate ownership: an aggregate lives in exactly one context. Employee lives in Workforce β€” Payroll references it by ID, never imports it. LedgerAccount/Entry live in Ledger β€” products read a projection, never the table.


5. Communication Matrix​

The strict rule:

  • gRPC β€” synchronous, in the user's request path, caller blocked on the answer.
  • Kafka β€” asynchronous, reaction to a fact that already happened; producer doesn't know consumers.
  • REST β€” external boundary only (frontends ↔ gateway, partner-facing public API). Never internal service-to-service.
  • Webhook β€” inbound from external parties (partner banks); verified (HMAC now, mTLS later) and immediately converted to an internal event.
From β†’ ToMechanismWhy
Frontend β†’ Gateway/BFFREST (+WS)Browser-native; versioned public surface
BFF β†’ any servicegRPCIn request path, needs the answer
Payroll β†’ Ledger (post entries)gRPCMust confirm balanced + idempotent before proceeding
Payroll β†’ Compliance (KYC ok?)gRPCGate decision blocks the flow
Payments-Orch β†’ Bank Gateway (initiate)gRPCNeeds accept/reject synchronously
Bank β†’ Bank Gateway (settlement)Webhook β†’ eventExternal, async; convert to disbursement.settled
Ledger β†’ world ("entry posted")KafkaFact; many react (recon, notify, products)
Run approved β†’ EWA/Lending recoverKafkaReaction, not in payroll's path
Anything β†’ NotificationsKafkaPure fan-out, must not block business tx
Anything β†’ AuditKafka (from outbox)Async durable record

MVP note: the mechanisms exist at MVP, but if a "service" is still a module in the shared deployable, its inbound calls are in-process behind the same gRPC-shaped port β€” so extraction later is a transport swap, not a redesign.

Anti-pattern bans (these create distributed monoliths):

  • ❌ No synchronous call chain deeper than 2 (Aβ†’Bβ†’C). Break with events.
  • ❌ No service queries another service's DB. Ever.
  • ❌ No REST between internal services.
  • ❌ No shared library containing another context's domain logic.

6. Event Architecture​

πŸ”΅ At MVP: demoz.<context>.<aggregate>.<event>.v<n> naming; one producer per topic; transactional outbox β†’ Kafka; real consumers wired (the current "publisher with zero consumers" is fixed); protobuf schemas in a Schema Registry with CI backward-compat gates; at-least-once + idempotent consumers; partition key = aggregate ID; per-consumer DLQ + alert.

🟑 Post-MVP: event catalog/registry site; replay tooling; CDC (Debezium) replacing the poller if throughput needs it.

βšͺ Long-term: tiered retention for ledger/audit logs; cross-region topic mirroring.

Ownership: the producing context owns the topic, its schema, and its evolution. One producer per topic. Consumers never write to another context's topic.

Versioning: additive changes = same major; breaking = new .v2 topic, dual-publish during migration, retire v1 after consumers move.

Delivery semantics: at-least-once + idempotent consumers (effective-once). Money consumers MUST be idempotent. Ordering: per-aggregate via partition key; cross-aggregate order is never assumed.


7. gRPC Contract Strategy​

packages/contracts 🟒 is the law and the highest-governed asset in the repo. Contract-first: proto PR β†’ buf generate (Go + TS committed) β†’ implement. buf breaking-change gate in CI. Consumer-driven contract tests are the safety net that makes the Payroll NestJS β†’ Go swap safe. Every RPC carries tenant + actor + idempotency metadata. This is the mechanism behind every "β†’ Go" in this document.


8. Kafka Strategy​

ConcernDecision
BrokerπŸ”΅ Redpanda (Kafka API, light ops) β†’ βšͺ managed Kafka at regional scale
Topicsdemoz.<context>.<aggregate>.<event>.v<n>; one owner
Partitions6–12 per topic; key by aggregate ID; over-partition money topics
Schemaprotobuf in registry, BACKWARD compat enforced in CI
ProducerTransactional outbox only β€” never produce directly from business code
ConsumerIdempotent (dedup on event ID), bounded retry β†’ DLQ topic
Retention7d hot for ops; infinite/tiered for ledger + audit (replay + compliance)
DLQper-consumer *.dlq + replay tooling + alert
Exactly-onceNot pursued cross-service; idempotent consumers = effective-once

Do not stand up a Kafka cluster you don't consume β€” wire the real consumers at MVP or it isn't an event architecture.


9. Database Ownership Matrix​

Rule (from MVP onward): one schema owner; no cross-context FK; no cross-context join; references by ID resolved via gRPC or event-fed projections.

StageReality
🟒 TodayOne shared Prisma schema across all domains (the central debt; cross-domain FKs exist)
πŸ”΅ MVPPer-context schemas in one Postgres instance β€” no cross-context FK/join; references by ID. (Cheap to do pre-launch; the spine of the migration.)
🟑 Post-MVPSeparate databases per extracted service (Ledger, Gateway, Payroll-Go, Notifications)
βšͺ Long-termLedger partitioned by (tenant, month), read replicas; shard by tenant at extreme scale
ServiceDatabase (target)Owned tablesCross-context refs (by ID only)
Identityiam_dbusers, sessions, credentialsβ€”
Tenancytenancy_dborgs, members, rolesuserId
Workforceworkforce_dbemployees, contractsorgId, userId
Ledgerledger_dbaccounts, transactions, entriestenantId, external refs
Payments-Orchpayments_dbsagas, disbursement_intentsrunId, accountId, ledgerTxId
Bank Gatewaygateway_dbdisbursements, webhook_events, recon_inputpartner refs
Payrollpayroll_dbruns, entries, rules, mandatesemployeeId, orgId
KYC/Screeningkyc_dbsubmissions, screeningsuserId, orgId
Products (EWA/Lending/Equb)<product>_dbproduct aggregatesemployeeId, ledgerAccountId
Notificationsnotif_dbmessages, deliveryrecipient refs
Auditaudit_dbappend-only audit logall (by ID)

10. Deployment Architecture (progressive β€” no enterprise infra early)​

StageDeployment
🟒 TodayDocker Compose (but Go services aren't in it β€” fix at MVP)
πŸ”΅ MVPCompose or a single small managed container host / tiny K8s; services co-deployed; Ledger + Bank Gateway run as real processes; secrets via cloud secret manager (not Vault yet); rolling deploys
🟑 Post-MVPSmall managed Kubernetes; namespaces by domain; HPA on CPU + Kafka lag; blue/green for Ledger; per-service CI (Nx affected)
βšͺ Long-termVault, service mesh (Linkerd) only if mTLS-everywhere / traffic-shifting demands it, canary (Argo), multi-region

Explicitly deferred: large K8s, service mesh, Temporal, Argo Rollouts. None are MVP. Add each when a concrete pain justifies it.


11. Security Architecture (progressive)​

πŸ”΅ MVP: OAuth2/OIDC + better-auth sessions at the gateway; centralized auth at the edge; HMAC on internal gRPC; Postgres RLS forced per context, proven under a non-superuser role in CI (today it's only tested under a superuser β€” launch blocker); cloud secret manager; TLS in transit; PII minimization in events (stop leaking email/PK).

🟑 Post-MVP: Vault dynamic creds; field-level PII encryption; crypto-shred for erasure.

βšͺ Long-term: mTLS service identity (supersedes HMAC); OPA policy; PCI-scoped isolated cluster if/when cards ship; NBE data-residency alignment.


12. Observability Architecture (progressive)​

  • πŸ”΅ MVP: structured JSON logs (trace-correlated), basic health/metrics, a few business alerts (disbursement success rate, DLQ depth, recon breaks).
  • 🟑 Post-MVP: full OpenTelemetry tracing across gRPC + Kafka headers, Prometheus + Grafana, SLO alerts, business dashboards.
  • βšͺ Long-term: Tempo/Loki at scale, chaos/GameDays.

Don't stand up the full stack before there's traffic to observe.


13. FinTech Compliance Considerations​

Source of truth = Ledger (record), reconciled against partner-bank statements (custody truth). Immutability (no DELETE, reversals as new entries) 🟒 enforced in ledger. Idempotency on every money command πŸ”΅. Audit trail in the same tx via outbox πŸ”΅. Reconciliation 🟑 proves money actually moved. SoD/maker-checker πŸ”΅ (CrossKindSoDPolicy). Custody clarity: orchestrator, not custodian (ADR-014) β€” Equb's simulated pool stays honestly labeled until a real partner-bank escrow account exists. Fraud hooks βšͺ. NBE/AML reporting βšͺ from the audit log.


14. Scaling Strategy​

Bottleneck order: Ledger writes (partition, replicas, batch postings per run, shard by tenant later) β†’ Payroll compute (parallel per employee β€” the reason to migrate to Go βšͺ) β†’ Bank Gateway (IO-bound; bulkhead per partner) β†’ Kafka (partition by aggregate). All services stateless; tenant is the shard key. Redis for sessions/read-models β€” never money truth. Build none of the sharding now; design the keys so it's possible later.

Target scale assumptions: ~5M users, ~200k businesses, thousands of payroll runs, millions of ledger entries/day.


15. Disaster Recovery Strategy​

  • πŸ”΅ MVP: daily snapshots + WAL archiving; tested restores; Kafka RF β‰₯ 3.
  • 🟑 Post-MVP: money-service RPO β‰ˆ 0 via streaming replica; documented RTOs; per-failure runbooks.
  • βšͺ Long-term: active-passive region; ledger rebuild-from-log; GameDays.

The Reconciliation against the partner bank is the ultimate backstop β€” even after a failure, the bank statement is ground truth for what moved.


16. Technology Recommendations (with migration paths)​

ConcernMVPEvolves toMigration path
PayrollNestJS πŸ”΅Go βšͺBehind stable gRPC proto; swap impl, consumers unaffected
IdentityNestJS πŸ”΅Go optional βšͺOnly if auth throughput demands; contract stable
LedgerGo 🟒Go (Rust only if proven)Already Go
Bank GatewayGo 🟒GoAlready Go; new bank = new adapter, same port
SyncgRPC + buf πŸ”΅sameβ€”
AsyncRedpanda πŸ”΅managed Kafka βšͺSame Kafka API
Orchestrationlightweight (DB state machine + outbox) 🟑Temporal βšͺIntroduce only when workflow complexity/scale justifies
DeployCompose / small host πŸ”΅small K8s 🟑 β†’ mesh βšͺProgressive
Secretscloud secret mgr πŸ”΅Vault πŸŸ‘β€”
Observabilitylogs + basic metrics πŸ”΅OTel + Grafana πŸŸ‘β€”

Hold the two-language ceiling (TS + Go; ADR-010). Rust is hypothetical, ledger-only, and only if Go can't meet latency.


17. New / Updated ADRs​

  • ADR-018 β€” Progressive extraction behind stable contracts (refines ADR-001; the launch-first principle).
  • ADR-019 β€” Database-per-service; per-context schemas at MVP, separate DBs on extraction (supersedes shared Prisma schema).
  • ADR-020 β€” Contract-first integration (buf + schema registry, CI gates).
  • ADR-021 β€” Lightweight saga orchestration at MVP; Temporal deferred until justified.
  • ADR-022 β€” Kafka naming / ownership / versioning / DLQ / replay (extends ADR-017).
  • ADR-023 β€” API Gateway + BFF; REST at edge only.
  • ADR-024 β€” Custody model: partner banks hold funds; Ledger is the record; no internal wallet (hardens ADR-006 + ADR-014). Prevents the Wallet drift from recurring.
  • ADR-025 β€” Bank Gateway adapter contract; bank-sandbox is a dev adapter, real FIs plug into the same port.

18. Risks​

RiskSeverityMitigation
Shared-schema carve-out done after launch (with live money)CriticalDo per-context schemas pre-launch
Half-extracted services (Go tier built but undeployed, Kafka unconsumed)Critical"Not a service until deployed + wired + tested"; fix at MVP
RLS validated only under superuserCriticalProve under non-superuser in CI before launch
Distributed-monolith (sync chains, cross-context SQL)HighEnforce boundary rules in CI (Nx)
Over-engineering MVP infraHighProgressive adoption; this doc's maturity labels
Wallet/custody confusion with regulators/partnersHighADR-024; honest Equb labeling; never claim custody
Saga money bugsHighIdempotency + reconciliation backstop now; Temporal later

19. Trade-offs​

Launch-first means accepting temporary co-deployment to gain speed, paying it back via stable contracts (cheap extraction later). Database-per-service loses cross-domain joins (replaced by gRPC/projections) to gain independence. Lightweight orchestration over Temporal trades some durability tooling for far less ops burden β€” acceptable while flows are simple, revisited when they aren't. Partner-bank custody trades direct control of funds for not being a regulated custodian β€” which is the entire DemozPay thesis, not a compromise.


20. Implementation Roadmap​

Phase 0 β€” Launch MVP (now β†’ ~8 wks). Carve shared schema β†’ per-context schemas (Identity, Tenancy, Workforce, Payroll, KYC). Deploy Ledger + Bank Gateway (Go) for real; bank-sandbox adapter behind the Gateway abstraction. Wire Kafka with real consumers (payroll β†’ repayment, β†’ notify, β†’ recon-input) + schema registry. Payroll stays NestJS behind a gRPC contract. Fix launch blockers: enable the disabled payroll consumer, fix webhook RLS, prove RLS under non-superuser, fix the gateway auth gap. No K8s / mesh / Temporal / Vault.

Phase 1 β€” First customers (~3–6 mo). Extract Payroll β†’ Go behind its proto (the polyglot proof). Extract Notifications (real service; delete the stub) and Reconciliation. Add Payments-Orchestrator (lightweight). Separate DBs for extracted services. Add OTel tracing, Grafana. Launch EWA/Lending/Equb as their products validate (code exists, flag-gated).

Phase 2 β€” Growth. Small managed Kubernetes, namespaces, HPA, blue/green for Ledger, Vault, per-service CI. Extract Screening/Fraud as load warrants. Introduce Temporal if saga complexity justifies.

Phase 3 β€” Regional scale. DR (tested restores, RF β‰₯ 3), active-passive region, add a second/third real bank adapter (Dashen/CBE/Telebirr/EthSwitch) behind the Gateway β€” and introduce Treasury to manage liquidity across them (its first real justification). New products (Savings, Merchant/QR, Bill) on stable rails.

Phase 4 β€” Millions of users. Shard Ledger by tenant; tiered Kafka; CQRS read models; multi-region active-active with data residency; mesh if needed; PCI-scoped cluster if Cards ship; Wallet considered as a product only if stored-value ever becomes the model.


Closing​

This blueprint keeps the 10-year platform vision intact while telling the truth about today: payroll on NestJS, no internal wallet, partner banks holding funds, the Ledger recording truth, the Bank Gateway as the swappable abstraction, and infrastructure adopted only as customers and load justify it. The permanent part is the contracts; everything else extracts on a schedule a startup can actually fund.


Related: SYSTEM_OVERVIEW.md Β· HANDBOOK.md Β· MONEY_FLOWS.md Β· BANK_ORCHESTRATION.md Β· ../adr/