03 — The codebase tour
A folder-by-folder walkthrough. After this you should be able to look at any file path and know what kind of code lives there.
Top-level shape
Demoz-Pay/
├── apps/ Process roots — bootstrap + wiring, NO business logic
├── packages/ Reusable code — bounded contexts + shared infra
├── services/ Independently deployable Go services
├── infra/ Terraform, Helm, ArgoCD, docker-compose, ops SQL
├── tooling/ ESLint rules, Prisma lint, Nx generators
├── docs/ ADRs, runbooks, architecture, onboarding (this folder)
├── nx.json, package.json, tsconfig.base.json, pnpm-workspace.yaml
└── docker-compose.yml, .env.example, CLAUDE.md, README.md, ...
The split is opinionated:
apps/are processes. They bootstrap. They wire. They never contain domain rules.packages/are libraries. Domain rules live here. Shared primitives live here.services/are standalone deployables. They have their owngo.mod,Dockerfile, and don't share build artefacts with the monolith.
Read docs/adr/ADR-001-modular-monolith.md
for why this split exists.
apps/ — six frontends + one API
apps/
├── api/ NestJS modular monolith (port 8000) [Live]
├── admin-web/ platform admin (Next.js 16) (port 4200) [Partial — UI mocks only]
├── employer-web/ employer / HR / finance (port 4201) [Partial — UI mocks only]
├── employee-web/ employee end-user (port 4202) [Partial — UI mocks only]
├── fi-web/ financial institutions (port 4203) [Partial — UI mocks only]
├── merchant-web/ BNPL merchants (port 4204) [Partial — UI mocks only]
├── docs-web/ public docs (Docusaurus) (port 4205) [Stub — template]
└── *-e2e/ Playwright tests per app [Stub — template tests]
Status legend: see ../STATUS_LEGEND.md.
apps/api/
The NestJS monolith. The most-important folder in the repo. We'll spend a section on it below.
apps/{admin,employer,employee,fi,merchant}-web/
Five Next.js 16 frontends. All five are UI shells with hardcoded
mock arrays in src/data/*.ts. None call the API. The route
trees are real (36 pages in admin, 29 in employer, 13 in employee,
10 in fi, 9 in merchant) but the data is fake.
Connecting these to the API is Planned. Until then, treat them
as design references, not implementations.
apps/docs-web/
Docusaurus template. The pages are the default Docusaurus tutorial
(create a blog post, deploy your site, etc.). No DemozPay docs.
Stub — should be deleted or filled.
apps/api/ — the NestJS monolith
This is where almost all backend work happens today.
apps/api/
├── src/
│ ├── main.ts ← entry point
│ ├── app/ ← root module wiring (AppModule, AppController)
│ ├── assets/ types/
│ │
│ ├── _infra/ ← cross-cutting plumbing (the transactional + platform spine)
│ │ ├── config/ ← Zod env validation + APP_CONFIG token
│ │ ├── prisma/ ← PrismaService (the DB client)
│ │ ├── shared-infra/ ← tx runner (sets app.tenant_id) + outbox + audit + idempotency
│ │ ├── observability/ ← tracing, pino-logger, /metrics + collectors
│ │ ├── health/ ← /healthz + /readyz + startup probes
│ │ ├── outbox/ ← outbox publisher worker
│ │ ├── dead-letter/ ← DLQ admin surface (requeue)
│ │ ├── scheduling/ ← leader election (advisory locks)
│ │ ├── notification-consumers/ ← in-process SMS/email dispatch
│ │ └── idempotency/ resilience/ email/ sms/ uploads/ grpc-auth/
│ │
│ ├── identity/ ← auth + tenancy + org management
│ │ ├── auth/ ← better-auth (factory, session.middleware, guards, @Public)
│ │ ├── tenant/ ← tenant-context.middleware (reads req.user.businessId)
│ │ ├── tenancy/ organization/ organization-provisioning/
│ │ └── members/ roles/ platform-staff/ merchant/ financial-institution/ me/
│ │
│ ├── workforce/ ← employee CRUD + child resources + tenant catalogs
│ │ ├── employee/ employee-absence/ employee-allowance/ employee-deduction/
│ │ └── department/ position/ employment-type/ payment-frequency/ absence-type/ allowance/ deduction/
│ │
│ ├── products/ ← ewa/ lending/ equb/ (adapter bindings; domain lives in packages/*)
│ ├── compliance/ ← kyc/ (incl. identity-verification) + sanctions/
│ ├── money/ ← banking/ + integration/ (bank-settlement applier, webhooks)
│ └── payroll/ ← payroll engine + consumers/ (was payroll-consumers)
└── prisma/
├── schema.prisma ← The whole DB schema
├── migrations/ ← 85+ numbered migrations
└── RLS.md ← Implementation pointer (canonical: ADR-013)
Reading order for the API
If you read these 6 files in this order, you'll understand 90% of how the API works:
apps/api/src/main.ts— boot sequenceapps/api/src/app/app.module.ts— what's wiredapps/api/src/_infra/config/config.module.ts+packages/shared/config/ src/lib/config.ts— the env contractapps/api/src/identity/auth/session.middleware.ts— howreq.useris populatedapps/api/src/identity/tenant/tenant-context.middleware.ts— how the tenant is resolvedapps/api/src/_infra/shared-infra/prisma-transaction-runner.ts— the transactional spine where everything money-related goes through
Then peek at:
packages/ewa/backend/application/request-ewa.usecase.tsto see a real use caseapps/api/src/products/ewa/ledger.grpc-client.tsto see how it talks to the Go ledger
services/ — three Go services
services/
├── ledger/ Go gRPC — money journal (port 50051)
│ ├── cmd/ledger/main.go
│ ├── internal/{config,pg,store,server,fingerprint}/
│ ├── migrations/ Its own Postgres schema (separate from API's)
│ ├── test/ Hermetic test harness (postgres:16-alpine on :5434)
│ ├── go.mod, Dockerfile, project.json, README.md
│ └── Status: [Partial — schema + DB invariants Live, Go server Compile-only]
│
├── integration-gateway/ Go HTTP — bank/wallet adapter hub
│ ├── cmd/integration-gateway/main.go (25 lines, /health only)
│ └── Status: [Stub]
│
└── notifications/ Go HTTP — SMS/email/push worker
├── cmd/notifications/main.go (25 lines, /health only)
└── Status: [Stub]
services/ledger/
The most-developed Go service. Implements the gRPC contract at
packages/contracts/grpc/ledger.proto:
PostTransaction— atomic multi-leg insert, idempotent via(tenant_id, idempotency_key)UNIQUE + fingerprint reconciliationGetBalance— derived fromledger_account_balanceviewReverse— compensating-entry RPC with double-reversal lockout (partial unique index +SELECT ... FOR UPDATE)GetEntries— paginated journal scanReconcileAccount— independent Go-side sum vs view-derived balance, returns drift
The schema is Live and runtime-proven (all 5 DB invariants
verified via psql on this host). The Go binary is Compile-only
on most hosts — you need Go 1.22+ to build it, and buf generate
to produce the proto stubs first.
The test harness at services/ledger/test/verify.sh brings up a
hermetic Postgres on :5434 and runs the store-layer integration
tests. Works without Go if you run it inside the container.
services/integration-gateway/
Stub. Intended to be the place where bank / wallet / mobile-money
adapters live (CBE, Telebirr, Awash, etc.). Today it serves
/health only.
services/notifications/
Stub. Intended to consume the outbox topic and send SMS / email /
push. Today it serves /health only. The SMS provider abstraction
currently lives at apps/api/src/identity/auth/sms-sender.ts (for the
better-auth phone-OTP flow); the eventual notifications service
would consume the outbox in Go.
packages/ — bounded contexts + shared infra + contracts
packages/
├── shared/ Cross-cutting infrastructure libraries
│ ├── money/ Exact-integer Money type [Live, 39 importers]
│ ├── idempotency/ Idempotency-Key store + port [Live, 15 importers]
│ ├── audit/ Audit emitter port [Live, 15 importers]
│ ├── events/ Outbox event types + EventPublisher port [Live, 18 importers]
│ ├── tenant-context/ AsyncLocalStorage tenant ctx [Live, 17 importers]
│ ├── auth/ JWT verifier port + FakeJwtVerifier [Deprecated — superseded by better-auth]
│ ├── database/ TransactionRunner port [Live, 15 importers]
│ ├── logging/ Logger contract [Live, 6 importers]
│ ├── validation/ Zod schemas [Partial — 4 importers]
│ ├── ui/ React design system [Partial — 2,624 LOC, ZERO TESTS, 73 importers]
│ └── config/ Zod env validation [Live, 10 importers]
├── ewa/ Earned Wage Access bounded context [Partial]
│ └── backend/
│ ├── domain/ Pure rules (Money, eligibility policy, status enum)
│ ├── application/ Use cases + ports (no Prisma here)
│ ├── infrastructure/ In-memory implementations (tests)
│ └── presentation/ EwaController, EwaModule, DTOs
├── lending/ Salary-backed loans bounded context [Partial]
│ └── backend/ Same hexagonal structure
└── contracts/ API contracts (the source of truth)
├── grpc/ ledger.proto, integration_gateway.proto
├── openapi/ (planned)
├── gen/ buf generate output [gitignored .pb.go files]
├── buf.yaml, buf.gen.yaml
└── README.md
packages/shared/ — the platform's primitives
Read packages/shared/README.md for the index. Highlights:
shared/money— never bypass this. It's the only correct way to do money arithmetic in the codebase.Money.fromMinor("150000", "ETB"),Money.allocate([60, 40]),Money.add,Money.sub, etc. All operations are exact-integer.shared/database— defines theTransactionRunner<TTx>port. Domain code depends on this interface;apps/api'sPrismaTransactionRunneris the concrete impl.shared/tenant-context—runWithTenant({tenantId}, fn)andgetTenantId(). Backed by Node'sAsyncLocalStorage. The middleware and runner use it to thread the tenant invisibly.shared/events— theEventPublisherport and the outbox event shape.shared/config— the Zod env schema. Single source for every environment variable in the platform.shared/auth—Deprecated. Only containsJwtVerifierinterface +FakeJwtVerifier. Real auth lives atapps/api/src/identity/auth/(better-auth). Has 4 importers, all likely test-only.shared/ui— 2,624 LOC of React components, zero tests, 73 importers. The frontends use it heavily. The widest-blast- radius untested code in the repo. Test coverage isPlanned(Phase 1 of the intern programme).
packages/ewa/backend/ and packages/lending/backend/
The two bounded contexts that exist as code today. Both follow the hexagonal shape described in ADR-003:
packages/ewa/backend/
├── domain/ Pure rules. No I/O. No framework.
│ ├── ewa-request.ts The aggregate root + invariants
│ ├── ewa-status.ts Status enum + transition rules
│ ├── eligibility.ts Policy: how much can this employee draw?
│ ├── errors.ts Domain-specific exceptions
│ └── events.ts Event payload types
├── application/ Use cases — orchestrate domain via ports.
│ ├── ports/
│ │ ├── ewa-request.repository.ts ← interface
│ │ ├── accrued-earnings.port.ts ← interface
│ │ ├── ledger.port.ts ← interface
│ │ ├── disbursement.port.ts ← interface
│ │ ├── eligibility-cache.port.ts ← interface
│ │ ├── ledger-accounts.port.ts ← interface
│ │ └── clock.port.ts ← interface
│ ├── request-ewa.usecase.ts
│ ├── disburse-ewa.usecase.ts
│ └── get-eligibility.usecase.ts
├── infrastructure/ Stunt-double impls for tests
│ ├── in-memory-ewa.repository.ts
│ └── system-clock.ts
├── presentation/ HTTP edge
│ ├── ewa.controller.ts ← @Controller('ewa')
│ ├── ewa.module.ts ← EwaModule.register()
│ ├── tokens.ts
│ └── dtos/
└── index.ts ← barrel exports
The pattern (from ADR-003):
domain/never imports anything outside itself. Pure TypeScript.application/depends ondomain/and on its own port interfaces.infrastructure/provides test doubles.presentation/is the framework adapter — defines controllers, modules.- The real production adapters (Prisma repos, gRPC clients) live
in
apps/api/src/products/ewa/, not here. This is whyapps/api/src/products/ewa/looks empty — it's just bindings.
packages/contracts/
.proto and OpenAPI sources. The Go stubs land in gen/go/ after
buf generate (gitignored — they're build artefacts).
buf.gen.yaml uses BSR remote plugins, so you don't need
protoc-gen-go installed locally. You do need buf itself.
The Go ledger consumes the stubs via a replace directive in
services/ledger/go.mod pointing at ../../packages/contracts/ gen/go. The gen/go/go.mod file is committed (the .pb.go files are
not) so the replace resolves immediately after buf generate.
infra/
infra/
├── docker-compose.yml (in repo root, not here — confusing, todo)
├── sql/
│ └── 00_create_outbox_publisher_role.sql [Live]
└── README.md
Today this folder is mostly aspirational. The only thing that
actually runs is the publisher-role provisioning SQL. Terraform,
Helm, ArgoCD subdirectories are mentioned in docs but don't exist
on disk. Treat infra/ as Stub until proven otherwise.
tooling/
tooling/
├── eslint/ Custom ESLint rules (e.g. no-delete-on-financial)
├── prisma-lint/ Schema lint rules (no float money, etc.)
├── generators/ Nx generators for new domains
└── README.md
Read the rules here before you write code. They catch common mistakes (DELETE on financial tables, float money columns, cross-domain imports). Most violations are caught at lint time.
docs/
docs/
├── STATUS_LEGEND.md ← read first; what Live/Partial/etc. mean
├── adr/ Architecture Decision Records 001…013
├── runbooks/ (mostly Stub — templates only today)
├── architecture/
│ └── restructure-2026-05.md ← historical log of the May 2026 refactor
├── onboarding/ ← this folder
├── archive/ 5 obsolete files queued for deletion in Phase 2C
└── security/ (planned home for compliance-ethiopia.md)
docs/adr/
The most important folder in this directory. 13 ADRs covering:
- ADR-001 modular monolith
- ADR-002 packages over libs
- ADR-003 domain package shape
- ADR-004 frontend rename
- ADR-005 money as santim
- ADR-006 ledger as source of truth
- ADR-007 idempotency-key required
- ADR-008 audit + outbox same transaction
- ADR-009 no DELETE on financial rows
- ADR-010 two-language ceiling
- ADR-011 cross-domain events only
- ADR-012 ledger accounting model
- ADR-013 tenant isolation via RLS
Every non-obvious choice has an ADR. Read the ones relevant to what you're touching.
docs/archive/
Five obsolete files:
DEMOZPAY_ARCHITECTURE.md— pre-restructure blueprint (delete)SYSTEM_DOCUMENTATION.md— pre-restructure overview (delete)NX_WORKSPACE_EXPLAINED.md— paths stale (delete)theme-system-plan.md— superseded bypackages/shared/ui(delete after UI ships)CORE_BANKING_MICROSERVICES_PLAN.md— contains Ethiopia regulatory research worth preserving. Pending carve-out todocs/security/compliance-ethiopia.mdbefore deletion.
Phase 2C of the doc consolidation plan handles these; nothing deleted yet.
Configuration files at the root
| File | What it does |
|---|---|
package.json | Workspace root; defines pnpm run scripts |
pnpm-workspace.yaml | Tells pnpm where workspace packages live |
nx.json | Nx workspace config — tags, target defaults, cache |
tsconfig.base.json | Shared TS config; defines @demoz-pay/* path aliases |
docker-compose.yml | Postgres + Redis + Redpanda + API + 5 webs (some have status caveats) |
.env.example | Template for env vars — copy and edit |
CLAUDE.md | AI session context |
README.md | Quick-start |
PROJECT_STRUCTURE.md | Intern-friendly long-form folder guide |
SECURITY_CONTROLS.md | Auditor Q&A reference |
INTERN_PROGRAM.md | 16-week intern programme |
Nx tags + module boundaries
Open nx.json and look at the project graph (pnpm graph). The
key insight:
- Projects are tagged with
scope:<domain>andtype:<role>. - ESLint module-boundary rule enforces:
scope:ewacannot importscope:lendingetc. (ADR-011). - If you try, lint fails before you can commit.
This is how cross-domain communication stays event-only without relying on developer discipline alone.
Continue reading
Next: 04-how-the-backend-works.md —
how all this fits together at runtime.