DemozPay — Executive System Overview
Snapshot date: 2026-05-31. Source of truth: code, schema, migrations, tests. Every claim below is traceable to a file:line. Where docs and code disagree, the code wins and the mismatch is flagged.
Companion documents in this set:
docs/architecture/HANDBOOK.md— architect's deep divedocs/onboarding/DEVELOPER_GUIDE.md— new-hire rampdocs/onboarding/DOMAIN_KNOWLEDGE_BASE.md— domain-by-domain business primerdocs/audits/CURRENT_STATE_AUDIT.md— Live/Partial/Stub/Dead inventorydocs/audits/API_INVENTORY_FRESH.md— every HTTP endpoint with authdocs/audits/ROADMAP_REALITY_CHECK.md— claims vs. reality
1. What DemozPay is
DemozPay is a payroll-linked fintech infrastructure platform for Ethiopia (CLAUDE.md:11, README.md:1). It is not a generic wallet, not a bank, and not a card issuer.
The core thesis (CLAUDE.md:9, README.md:5):
Payroll is the trust anchor. Income history is the underwriting signal. Financial access is the product surface.
The intended end-to-end model is payroll → wallet → EWA → salary-backed loans → BNPL → Equb/savings, where wallet ([Planned]) is a read-only projection of the employee's ledger position (available headroom, outstanding advances) — not a fund-holding account (DemozPay is an orchestrator, never a custodian — ADR-014; wallet is derived from the ledger — ADR-006). Of these, payroll, EWA, lending, KYC, sanctions, and Equb have code today. BNPL, generic Wallet, and Savings remain Planned — see §3 below.
Architectural posture (ADR-014, docs/adr/ADR-014-orchestrator-not-custodian.md):
DemozPay is a bank-to-bank orchestrator, NOT a custodian of customer cash. Money lives in partner banks. DemozPay owns the journal of who owes what to whom, and the orchestration of moving it. This shapes every other ADR.
2. Core business domains
┌──────────────────────────────────────────────────────────────────┐
│ DemozPay platform │
│ │
│ ┌──────────┐ ┌─────┐ ┌─────────┐ ┌──────┐ │
│ │ Payroll │──▶│ EWA │ │ Lending │ │ Equb │ │
│ │ (E3) │ │ │ │ │ │ │ │
│ └────┬─────┘ └──┬──┘ └────┬────┘ └──┬───┘ │
│ │ │ │ │ │
│ └────────────┴───────────┴────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ KYC + Sanctions (gate every disbursement) │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Ledger (Go, double-entry, append-only) │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Integration-Gateway (Go, partner banks) │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Six implemented domains (one package each under packages/):
| Domain | Package | Aggregates | Purpose |
|---|---|---|---|
| Payroll | packages/payroll/ | 41+ across 8 subdomains (PayrollRun, SalaryComponent, DeductionMandate, OvertimePolicy, TaxRule, PensionRule, PayrollAdjustment, PayrollEmployeeTransfer) | Compute net pay, emit deductions for EWA + lending repayment |
| EWA | packages/ewa/ | EwaRequest | Earned-wage access between paydays, repaid via payroll deduction |
| Lending | packages/lending/ | Loan, LoanRepayment, RepaymentSchedule | Salary-backed loans with installment repayment via payroll |
| KYC | packages/kyc/ | KycSubmission, KycDocument, NationalId | Identity verification, prerequisite for every disbursement |
| Sanctions | packages/sanctions/ | ScreeningCheck, SanctionsListEntry | OFAC/UN/Ethiopia screening, prerequisite for KYC approval + Equb payout |
| Equb | packages/equb/ | EqubCycle, Draw | Rotating-savings pool (Ethiopian cultural finance product) with cryptographic lottery |
Planned but not implemented (zero code):
- BNPL (
packages/bnpl/— does not exist) - Generic Wallet (per ADR-006 will be a read-only view over the ledger, not a separate aggregate)
- Savings (
packages/savings/— does not exist)
3. Status matrix (LIVE / PARTIAL / PLANNED)
Status tags follow docs/STATUS_LEGEND.md — only Live if runtime-verified on at least one host.
Backend domains (under packages/)
| Domain | Status | Evidence |
|---|---|---|
| EWA | Live | 4 use cases, 8 ports, 7 outbox events, 100% use-case test coverage; HTTP controller apps/api/src/products/ewa/ewa-api.module.ts |
| Lending | Live | 5 use cases (request-loan, quote-loan, disburse-loan, record-repayment, remit-installment-to-fi), 9 ports, 8 outbox events; apps/api/src/products/lending/ |
| KYC | Live (backend) | 6 use cases, 5 ports, 6 events, prisma adapter + HTTP controller; gates EWA + lending disburse + Equb payout |
| Sanctions | Live (backend) | 2 use cases, OFAC/UN/Ethiopia ingest CLI (apps/api/src/compliance/sanctions/cmd/); HTTP screening + CSV ingest |
| Payroll | Live (backend) | 38 use cases, 20 ports, 22+ outbox events; HTTP controllers (audit, settlement, PDF, court-orders, tenant-settings, auto-lock, platform-admin); 24 Prisma models |
| Equb | Live (Phase 1 Alpha) | 8 use cases, 5 ports, 8 events; PRIVATE invitation flow (T2-J) + cryptographic draw (commit/reveal); partner-bank escrow binding |
Infrastructure
| Capability | Status | Evidence |
|---|---|---|
| NestJS modular monolith boot | Live | apps/api/src/main.ts:87 global prefix api, three APP_GUARDs chained, /healthz + /readyz real |
| Prisma + Postgres | Live | 73 models, 41 migrations all applied (apps/api/prisma/migrations/) |
| Tenant isolation (RLS) | Live | Forced on 20+ financial tables (20260526030000_apply_tenant_rls/migration.sql); verify guards roll back on missing policy |
| Ledger schema + invariants | Live | Append-only triggers, deferred sum-to-zero, partial unique on reverses_transaction_id (services/ledger/migrations/0001_init.up.sql) |
| Ledger Go server | Live | 8 RPCs implemented (services/ledger/internal/server/); HMAC auth verified (services/ledger/cmd/ledger/main.go:124-145); 7 store integration tests pass |
| Integration-gateway Go server | Live | 4 RPCs (Initiate, Lookup, Status, AdapterStatus); mock + dashen adapters; reconciliation runner (cmd/recon-runner/); webhook receiver |
| Bank-sandbox dev harness | Live | 6 HTTP routes + failure-injection by account prefix (services/bank-sandbox/internal/handler/handler.go) |
| Notifications Go service | Stub | services/notifications/cmd/notifications/main.go is /health-only — no Kafka consumer, no SMS dispatch |
| In-process notification dispatcher | Live | apps/api/src/_infra/notification-consumers/ polls outbox + dispatches via SMS/email modules |
| Outbox + audit (single txn) | Live | OutboxEvent + AuditEntry models written in same transaction as state change (ADR-008) |
| Better-auth (email + phone OTP + org) | Live | apps/api/src/identity/auth/, plugins wired; email verification + password reset live via pluggable email module |
| Better-auth 2FA (TOTP) | Partial | TwoFactor Prisma model exists, plugin not yet wired (docs/STATUS_LEGEND.md:21 "2FA Planned"); AdminMfaGuard enforces TOTP on @RequirePlatformAdmin routes |
| gRPC service-to-service HMAC auth | Live | TS signer (apps/api/src/_infra/grpc-auth/grpc-hmac.ts), Go verifier (packages/grpcauth-go/), defaults to log-only with strict-mode rollout |
Prometheus /metrics | Live | API monolith (/api/metrics) + ledger (:50054/metrics) + integration-gateway (:50053/metrics) |
| Bank-statement reconciliation | Partial | Dashen CSV ingester + matcher + runner exist (services/integration-gateway/cmd/recon-runner/main.go); cron schedule pending |
Frontends (apps/*-web)
| App | Status | Evidence |
|---|---|---|
admin-web | Partial — UI shell only | Next.js scaffolds; data is hardcoded mocks (per CLAUDE.md:24) |
employer-web, employee-web, fi-web, merchant-web | Partial — UI shell only | Same shape as admin-web |
docs-web | Stub | Docusaurus template, no content |
Planned (zero code)
| Capability | Where it would live | Status note |
|---|---|---|
| BNPL domain | packages/bnpl/ | Not in codebase. Legacy Prisma models BNPLPurchase/BNPLPayment/Merchant exist but use Decimal(15,2) (ADR-005 violation) and have zero callers. (BNPLPartner was collapsed into Merchant in migration 20260613140000.) |
| Generic Wallet aggregate | packages/wallet/ | Per ADR-006 wallet is a derived read over the ledger, not a separate aggregate. Existing Wallet Prisma model is @deprecated. |
| Savings | packages/savings/ | Not in codebase. |
| mTLS service-to-service | infra | Year-1 work behind cert-rotation runbook (docs/security/AUTH_MIGRATION_STRATEGY.md). HMAC is the current intermediate. |
| Adjustment-journal process | implementation of ADR-015 | ADR-015 is Proposed; no code yet. |
| Dispute workflow | packages/dispute/ (ADR-016) | ADR-016 is Proposed; no code yet. |
4. Current product capabilities (what you could actually do today, end-to-end)
The runtime story today is infrastructure-complete, product-thin. You can:
- Create an organisation and sign in via email + password (better-auth,
Live). - Onboard employees (
POST /api/employees,apps/api/src/workforce/employee/employee.controller.ts). - Submit + approve KYC with sanctions screening (
apps/api/src/compliance/kyc/,apps/api/src/compliance/sanctions/). - Compute a payroll run (CRUD salary components, tax rules, deduction mandates, then
calculate-payroll-run→approve→disburse). Emitspayroll.deductions_taken.v1consumed by EWA/lending. End-to-end as of E3 2026-05-29 per CLAUDE.md:35. - Issue an EWA advance (
request-ewa→disburse-ewagated by KYC + account lookup;record-ewa-repaymentties to payroll deduction). - Issue a loan with full lifecycle (
request-loan→quote-loan→disburse-loan→record-repayment→remit-installment-to-fi). - Run an Equb cycle (CORPORATE or PRIVATE with invitations + commit/reveal draw + payout gated by KYC + sanctions; partner-bank escrow binding).
- Reconcile a bank statement vs the ledger (CSV ingest, matcher flags drift).
What you cannot do today:
- Send money to a real bank from a real customer. The integration-gateway has Go RPCs implemented; the mock and dashen adapters are the only registered partners.
dashenreturnsErrNotConfiguredwithoutGATEWAY_DASHEN_BASE_URL+GATEWAY_DASHEN_SIGNING_KEY— i.e., no real Dashen contract is live in this repo. - Buy-now-pay-later anything.
- Pay a salary through a real bank rail end-to-end (the Go gateway works, the partner contract doesn't exist).
5. Major money flows
6. How payroll supports the rest of the platform
Payroll is the trust anchor + repayment rail for the entire product surface.
- Trust anchor. Employers run payroll on DemozPay; the run engine emits
payroll.deductions_taken.v1per entry onApprovePayrollRun. That event is the canonical signal that an employee has earned X net pay this period (packages/payroll/.../events.ts:19). - Repayment rail. EWA + lending both subscribe to
payroll.deductions_taken.v1and use it to close out their receivables (packages/lending/.../record-repayment.usecase.ts,packages/ewa/.../record-ewa-repayment.usecase.ts). Lending also reads the deduction-mandate registry to know which installment to attribute. The integration is wired through outbox + Kafka-style consumers (apps/api/src/payroll/consumers/). - Underwriting signal. Lending's
IncomePortandEqubBehaviorSignalPorttogether build a credit picture (income history + Equb participation reliability). Both are derivative of payroll data. - Cash-flow truth. The Ledger's
payroll-clearingaccount is the per-employer holding account that payroll debits + the disburse-side credits. Reconciliation drift betweenpayroll-clearingand the partner bank's statement is the daily go-live monitoring signal (services/ledger/internal/server/reconcile_with_bank.go).
The architecture inverts the usual fintech onboarding funnel — instead of "sign up, deposit, then we offer you products," the funnel is "your employer runs payroll on us → we already know your income → you qualify for EWA + a loan today."
7. Per-domain summary
The docs/onboarding/DOMAIN_KNOWLEDGE_BASE.md companion goes deep. Here is the one-page version:
Payroll [Live]
24 Prisma models (PayrollRun, PayrollRunEntry, PayrollTaxRule, PayrollPensionRule, PayrollOvertimePolicy, …, PayrollEmployeeTransfer). Lifecycle DRAFT → CALCULATED → APPROVED → LOCKED → DISBURSED. Locks block all mutation (20260530200000_payroll_run_immutability/migration.sql). Court-order remit + auto-lock-policy + tenant-settings live.
Ledger [Live]
Go service. Double-entry, deferred-trigger balanced commit, append-only triggers on ledger_transaction + ledger_entry, RLS-forced. 8 RPCs (PostTransaction, GetBalance, GetEntries, Reverse, ReconcileAccount, ReconcileWithBank, ConfirmSettlement, MarkSettlementFailed). 7 store integration tests.
Wallet [Planned]
No packages/wallet/. Per ADR-006 wallet is derived from the ledger, not a separate aggregate. The legacy Wallet/WalletTransaction/WithdrawalRequest Prisma models are @deprecated (apps/api/prisma/schema.prisma:797,828,865), have zero callers, and remain only for archival.
EWA [Live]
Earned-wage access. Aggregate EwaRequest, 4 use cases, 7 events. Disburse gated by KYC + sanctions + account-lookup. Repayment via payroll deduction.
Lending [Live]
Salary-backed loans. Aggregates Loan, LoanRepayment, RepaymentSchedule. Underwriting consumes Equb + payroll signals. Installment-level remit to financial institutions (remit-installment-to-fi.usecase.ts).
BNPL [Planned]
No packages/bnpl/. Legacy Prisma models exist (BNPLPurchase, BNPLPayment, Merchant) but use Decimal(15,2) (ADR-005 violation), have no tenantId, are not under RLS, and have zero callers. (BNPLPartner was collapsed into Merchant; in v1 the merchant IS the BNPL-accepting entity — no separate underwriter org.)
Equb [Live — Phase 1 Alpha]
Rotating-savings circle. EqubCycle aggregate, draw via SHA-256 commit/reveal (seed hash committed at activation, seed + nonce revealed at draw). 6 Prisma models including PartnerBankEscrowBinding + EqubReconciliationRun (Phase 1b). PRIVATE flow includes invitation lifecycle.
Savings [Planned]
No packages/savings/. Legacy SavingGoal model exists; Decimal(15,2); zero callers.
Treasury [Not modelled]
There is no packages/treasury/ and no domain entity called "treasury". The closest construct is the partner-bank escrow binding (PartnerBankEscrowBinding) for Equb pools, plus the per-employer payroll-clearing ledger account. No automated cash-management or float-deployment surface exists.
Settlement [Live as a ledger primitive]
Settlement is the ledger's PENDING → POSTED state transition (ConfirmSettlement RPC) driven by partner-bank webhooks at the integration-gateway. Legacy SettlementBatch + SettlementRecord Prisma models exist for the dead BNPL/lending lineage and have zero callers.
8. High-level component diagram
9. Engineering stance (non-negotiables)
These are enforced in CI, in the Postgres triggers, or both:
- Money is
NUMERIC(20,0)in santim (1 ETB = 100 santim). Never floats. (ADR-005,Live) - Ledger is sole source of money truth. No derived
balancecolumns on new schema. (ADR-006,Livefor new schema; legacyWallet.balanceandLedgerAccount.balanceareDeferredfor removal.) Idempotency-Keyrequired on every money-moving POST. (ADR-007,Partialat the API gateway,Liveat the ledger.)- Audit + outbox event live in same DB transaction as the state change. (ADR-008,
Live) - No
DELETEon financial rows. Reversals create new entries. (ADR-009,Live— DB triggers + ESLint guard.) - Tenant scoping mandatory via Postgres RLS forced on every financial table. (ADR-013,
Live— runtime-proven by verify guards.) - No cross-domain imports. Cross-domain communication via outbox events. (ADR-011,
Live— Nx + ESLint module boundaries.) - Two-language ceiling: TypeScript + Go. (ADR-010,
Live)
10. Reading order for a new contributor
- This document.
docs/onboarding/DEVELOPER_GUIDE.md— run it locally.docs/onboarding/DOMAIN_KNOWLEDGE_BASE.md— understand the business.docs/architecture/HANDBOOK.md— internalise the patterns.docs/adr/*in numeric order — the "why" behind every rule.docs/audits/CURRENT_STATE_AUDIT.md— what is and isn't real.