DemozPay — Developer Onboarding Guide
Audience: interns, junior devs, new engineers. Pair this with
docs/architecture/SYSTEM_OVERVIEW.md(what the platform is) anddocs/onboarding/DOMAIN_KNOWLEDGE_BASE.md(why the platform exists).Snapshot: 2026-05-31. If a command doesn't work, the code is right and this doc is stale — open a PR.
1. Prerequisites
Confirmed from package.json + apps/api/Dockerfile + Go modules:
| Tool | Required | How to check |
|---|---|---|
| Node.js | 22.13+ | node -v (pnpm@11 refuses lower; you'll see requires at least Node.js v22.13) |
| pnpm | 11.x | corepack enable && corepack prepare pnpm@11.4.0 --activate |
| Docker + docker-compose | latest | docker --version && docker compose version |
Postgres client psql | for ledger/gateway migrations | psql --version |
| Go | 1.23+ | go version (or use the bundled toolchain at .tools/go/bin/go) |
| buf | optional (only if regenerating protos) | buf --version |
You do not need a global Java/Python/Ruby — see ADR-010 (two-language ceiling).
2. Run locally — the fast path (full docker-compose stack)
Source: README.md:42-112, infra/docker-compose.full.yml. 17 containers, one command.
git clone <repository-url> Demoz-Pay
cd Demoz-Pay
# 1. boot everything (first run ~5 min)
docker compose -p demoz -f infra/docker-compose.full.yml up -d --build
# 2. apply API DB migrations
docker compose -p demoz -f infra/docker-compose.full.yml --profile migrate \
run --rm api-migrate
# 3. apply ledger + gateway DB migrations (raw SQL)
psql postgresql://ledger:ledger@localhost:5441/ledger \
-f services/ledger/migrations/0001_init.up.sql \
-f services/ledger/migrations/0002_reverses_unique.up.sql \
-f services/ledger/migrations/0003_pending_posted.up.sql \
-f services/ledger/migrations/0004_allow_metadata_mutation.up.sql
psql postgresql://gateway:gateway@localhost:5442/gateway \
-f services/integration-gateway/migrations/0001_init.up.sql \
-f services/integration-gateway/migrations/0002_bank_statement_line.up.sql
# 4. verify
curl http://localhost:3030/api/healthz # api
curl http://localhost:50054/healthz # ledger
curl http://localhost:50053/healthz # integration-gateway
curl http://localhost:8088/healthz # bank-sandbox
Ports (deliberately offset to coexist with telemedhin):
| Service | URL |
|---|---|
apps/api | http://localhost:3030 (NOT :3000) |
| ledger gRPC | localhost:50051 |
| ledger HTTP | http://localhost:50054 |
| integration-gateway gRPC | localhost:50052 |
| integration-gateway HTTP | http://localhost:50053 |
| bank-sandbox | http://localhost:8088 |
| Postgres (api) | localhost:5440 |
| Postgres (ledger) | localhost:5441 |
| Postgres (gateway) | localhost:5442 |
| frontends | http://localhost:4200..4204 |
| Prometheus | http://localhost:9090 |
| Redpanda Console | http://localhost:9080 |
Tear down (wipes volumes too): docker compose -p demoz -f infra/docker-compose.full.yml down -v.
3. Run locally — host-machine path (faster inner-loop)
For day-to-day backend development you'll want apps/api running on the host (instant restarts) and only the dependencies in Docker.
# 1. install deps
pnpm install
# 2. boot just Postgres + Redpanda
pnpm docker:up
# 3. apply Prisma migrations
pnpm prisma:migrate
# 4. start the API
pnpm dev:api # NestJS on :3030
# Or start everything:
pnpm dev:all
For the Go services on the host:
# Ledger
cd services/ledger
LEDGER_DATABASE_URL=postgresql://ledger:ledger@localhost:5441/ledger \
go run ./cmd/ledger
# Gateway
cd services/integration-gateway
GATEWAY_DATABASE_URL=postgresql://gateway:gateway@localhost:5442/gateway \
go run ./cmd/integration-gateway
4. Environment variables
AppConfig is the typed contract (packages/shared/config/src/lib/config.ts). Highlights:
| Var | What it does | Default |
|---|---|---|
DATABASE_URL | Prisma DSN for apps/api | required |
OUTBOX_DATABASE_URL | DSN for the outbox publisher (BYPASSRLS role) | required if publisher enabled |
BETTER_AUTH_SECRET | session encryption | required |
BETTER_AUTH_URL | public base URL (e.g. http://localhost:3030) | required |
LEDGER_GRPC_ADDR | where apps/api dials the ledger | localhost:50051 |
INTEGRATION_GATEWAY_GRPC_ADDR | where apps/api dials the gateway | localhost:50052 |
LEDGER_PROTO_PATH | absolute path to ledger.proto | <cwd>/packages/contracts/grpc/ledger.proto |
EMAIL_PROVIDER | logger (dev) / smtp (prod) / http | logger |
SMS_PROVIDER | logger (dev) / ethio-telecom / http | logger (refused in prod by apps/api/src/_infra/sms/sms.module.ts boot guard) |
GRPC_LEDGER_AUTH_SECRET, GRPC_GATEWAY_AUTH_SECRET | TS-side HMAC signing secrets for R1 | empty disables signing |
GRPC_LEDGER_AUTH_CLIENT_ID, GRPC_GATEWAY_AUTH_CLIENT_ID | client identifier in the signed metadata | api |
OUTBOX_PUBLISHER_ENABLED | outbox → Kafka publisher | false |
EQUB_LEDGER_RECONCILE_VIA_GRPC | swap equb reconciliation to Go RPC instead of in-process equb tables | false |
Go services (per their READMEs):
| Var | Service | Notes |
|---|---|---|
LEDGER_DATABASE_URL | ledger | role must NOT have BYPASSRLS |
LEDGER_GRPC_ADDR | ledger | default :50051 |
LEDGER_HTTP_ADDR | ledger | default :50054 |
GATEWAY_DATABASE_URL | gateway | role must NOT have BYPASSRLS |
GATEWAY_ENABLED_PARTNERS | gateway | comma list; default mock |
GATEWAY_DASHEN_BASE_URL, GATEWAY_DASHEN_SIGNING_KEY | gateway | required if dashen is enabled |
GRPC_AUTH_CLIENTS | both | JSON {clientId: secret}; ≥16-char secrets enforced |
GRPC_AUTH_MODE | both | disabled / log-only (default if clients set) / strict |
5. Database
5.1 Migration workflow
# generate Prisma client + types
pnpm prisma:generate
# apply pending migrations (dev)
pnpm prisma:migrate
# create a new migration after editing schema.prisma
node_modules/.bin/prisma migrate dev \
--schema apps/api/prisma/schema.prisma \
--name <short_snake_case_name>
# open Prisma Studio
pnpm prisma:studio
Rules:
- Every new financial table must be
tenantId-scoped and haveALTER TABLE … FORCE ROW LEVEL SECURITY+ atenant_isolationpolicy. Seeapps/api/prisma/migrations/20260526030000_apply_tenant_rls/migration.sqlfor the canonical shape. - Money columns:
BigInt @db.Numeric(20,0)(santim). NeverFloat, neverDecimal(15,2)on new tables. - New migrations must include a verify guard — a
DO $$ … RAISE EXCEPTIONblock that rolls back the migration if the expected RLS policy isn't in place. Example:20260529300000_payroll_run/migration.sql.
5.2 Ledger + gateway migrations
The Go services own their own Postgres clusters. They do NOT use Prisma; migrations are raw SQL files applied by hand (above) or by an operator-run job. CI does not migrate them automatically.
5.3 Resetting your local DB
docker compose -p demoz -f infra/docker-compose.full.yml down -v # wipes volumes
docker compose -p demoz -f infra/docker-compose.full.yml up -d
# then re-apply all migrations
6. Testing
6.1 Commands
pnpm test # all unit tests
pnpm nx test <project> # one project (e.g. pnpm nx test shared-money)
pnpm nx affected -t test # only changed projects
pnpm nx test api # full apps/api Jest run
RUN_INTEGRATION=1 pnpm nx test api # include integration specs (require Postgres up)
For Go:
cd services/ledger && go test ./...
cd services/integration-gateway && go test ./...
cd packages/grpcauth-go && go test ./...
6.2 What's tested
Counts (verified by listing *.spec.ts / *_test.go):
| Package | Unit specs | Integration specs |
|---|---|---|
packages/ewa/ | 5 | 0 |
packages/lending/ | 6 | 0 |
packages/kyc/ | 4 | 0 |
packages/sanctions/ | 2 | 0 |
packages/payroll/ | 34 | 0 (3 live at apps/api level) |
packages/equb/ | 5 | 0 |
apps/api/src/payroll/consumers/ | 8 | 2 (payroll-deductions-poller, court-order-auto-submit) |
apps/api/src/payroll/ | 11 | 1 (payroll-run-immutability-trigger) |
services/ledger/ | (Go) 7 store integration tests | |
services/integration-gateway/ | (Go) matcher / runner / ingester / lookup | |
packages/grpcauth-go/ | (Go) verifier + interceptor |
Test gaps to be aware of (full list in docs/audits/CURRENT_STATE_AUDIT.md): several KYC use cases (approve, claim-for-review, request-more-info), the sanctions list ingester, several Equb use cases (draw, invitation-accept, contribution, payout).
6.3 Patterns
- Domain + application tests use in-memory adapters (each domain package ships its own
in-memory-*.repository.ts). - Prisma adapters live in
apps/api/src/<domain>/and are exercised by integration tests behindRUN_INTEGRATION=1. - gRPC clients are unit-tested via dependency injection — the proto loader is real; the underlying gRPC channel is mocked.
7. Coding standards
7.1 The non-negotiable rules
The ADRs are the law:
| ADR | Rule |
|---|---|
| 005 | Money is NUMERIC(20,0) santim. Never Float. Use @demoz-pay/shared-money. |
| 006 | Ledger is sole source of money truth. No new balance columns. |
| 007 | Idempotency-Key required on every money-moving POST. |
| 008 | Audit + outbox event + state change live in the same DB transaction. |
| 009 | No DELETE on financial rows. Reversals create new entries. |
| 010 | TypeScript + Go only. |
| 011 | No cross-domain imports. Outbox events for cross-domain communication. |
| 013 | Every tenant-scoped query must run under a pinned tenant_id. RLS is FORCEd. |
7.2 Style
- No comments explaining what code does — well-named identifiers do that. Comments only for why (constraints, invariants, surprising behaviour).
- No backwards-compatibility shims unless the user is actively migrating off something. Delete unused code.
- No premature abstractions. Three similar lines beats a clever generic. Pull-out only when a third caller arrives.
- No console.log in committed code (the logger lives in
@demoz-pay/shared-logging).
7.3 Domain package layout
Every new domain follows packages/<domain>/backend/{domain, application, infrastructure, presentation}/ per ADR-003. Prisma may NOT be imported under backend/domain/ or backend/application/ — only infrastructure/.
7.4 New endpoints
When you add an HTTP endpoint to apps/api:
- Use
@Public()only for unauthenticated routes (health, metrics, public webhooks with HMAC). - Use
@RequireOrgRole('admin','owner')for tenant-admin actions. - Use
@RequirePlatformAdmin()for cross-tenant operator actions (TOTP enforced). - Money-moving POST: require
Idempotency-Keyheader; thread it to the ledger gRPC. - Add a row to
docs/audits/API_INVENTORY_FRESH.mdin the same PR.
8. Pull request process
8.1 Branch strategy
- Default branch:
main. - Feature branches:
feat/<short-slug>,fix/<slug>,chore/<slug>,docs/<slug>,restructure/<slug>. - Long-running refactor branches are acceptable but rebase weekly.
8.2 Commit messages
Conventional commits in lowercase, scope optional: feat(equb): implement private-cycle invitation flow. Recent examples (git log --oneline):
b6a2c8d feat: implement notification dispatcher service and related components
3ef4423 feat(equb): implement KYC and sanctions compliance checks for payouts
8f35774 feat(payroll): implement PayrollEmployeeTransfer aggregate and related functionality
8.3 PR checklist
- All tests pass (
pnpm nx affected -t test). - Lint passes (
pnpm lint). - If you changed schema: migration created with verify-block, RLS policy if financial.
- If you added an endpoint: row added to API inventory + auth decorators in place.
- If you added an outbox event: event name lands in the package's
events.tsenum. - If you broke an ADR: write the ADR that supersedes it.
8.4 Review process
- One reviewer minimum; two if it touches money, schema migrations, or auth.
- The reviewer checks: ADR compliance, evidence the new code is exercised (test or runbook), no Prisma in domain/application, no cross-domain imports.
8.5 Things never to do without explicit approval
- Force-push to
main. - Add a new programming language.
- Add a new
balancecolumn. - Add a
FloatorDecimalmoney column to a new table. - Skip RLS on a tenant-scoped table.
- Delete a financial row (or write code that could).
9. First-week learning path
This assumes you can finish §2-3 the day before Day 1.
Day 1 — orient
- Read
README.md+CLAUDE.md+docs/STATUS_LEGEND.md. - Read
docs/architecture/SYSTEM_OVERVIEW.md. - Get the full Docker-compose stack running (§2 above).
- Curl every health endpoint until they return 200.
- Open Prisma Studio. Walk through the models:
PayrollRun,EwaRequest,Loan,EqubCycle,KycSubmission,OutboxEvent,AuditEntry. - Open
docs/adr/and read all 16 ADRs. They are deliberately short.
Day 2 — see the EWA flow end-to-end
- Read
packages/ewa/backend/domain/ewa-request.ts(the aggregate). - Read all 4 use cases under
packages/ewa/backend/application/use-cases/. - Read
apps/api/src/products/ewa/ewa-api.module.ts— see how the package's ports are wired to real Prisma + gRPC adapters. - Run the EWA test suite:
pnpm nx test ewa. - Trace a request:
- HTTP comes in →
EwaController(in apps/api/src/products/ewa/). - Calls
RequestEwa.execute(...). - Use case calls
DisbursementPort.disburse(...)→IntegrationGatewayClient→ gRPC. - Use case writes
EwaRequestrepository +OutboxEventin same Prisma transaction. - Outbox poller ships
ewa.requested.v1to Kafka (if enabled).
- HTTP comes in →
Day 3 — see the ledger from both sides
- Read
services/ledger/migrations/0001_init.up.sql(deferred-trigger balanced commit, append-only triggers, RLS). - Read
services/ledger/internal/server/post_transaction.go— the only money-moving entry point. - Run the ledger Go tests:
cd services/ledger && go test ./.... - Open
apps/api/src/products/ewa/ledger.grpc-client.ts— see how TS calls the Go ledger. Note the HMAC signing. - Open
packages/grpcauth-go/hmac.go— the Go verifier. - Read
services/ledger/README.md§Service-to-service auth.
Day 4 — the payroll engine
- Read
docs/architecture/PAYROLL_ARCHITECTURE.md(existing in repo). - Walk
packages/payroll/backend/domain/— 41 aggregates across 8 subdomains. Don't try to memorise; just notice the boundaries (compensation, deductions, earnings, overtime, rules, protection, adjustments, disbursement). - Read
packages/payroll/.../use-cases/calculate-payroll-run.usecase.ts— the heart of the engine. - Read
apps/api/src/payroll/payroll-api.module.ts— see the 8 controllers. - Run
pnpm nx test payroll(34 specs).
Day 5 — pick a small bug or doc fix
- Open
docs/audits/CURRENT_STATE_AUDIT.mdand find a test gap (e.g. an Equb use case that lacks a spec). - Write the test. Run it. Get it green.
- Open a PR with a meaningful message. Wait for review.
- If you ran into anything misleading in these docs, fix the docs in the same PR — that is how the docs stay alive.
After this week you'll be able to:
- Run any subset of the stack from scratch.
- Read any aggregate without asking "what does this do?".
- Find any HTTP endpoint via
docs/audits/API_INVENTORY_FRESH.md. - Trace a money flow from HTTP → domain → ledger → bank.
10. Where to ask for help
In order:
- Read the ADR (
docs/adr/). - Read the per-domain README (
packages/<domain>/README.mdwhere it exists;services/<svc>/README.md). - Read the runbook (
docs/runbooks/— mostly stubs today; flag missing runbooks). - Ask a teammate. The codebase isn't big — most questions have an in-codebase answer.
Do not copy patterns from docs/architecture/restructure-2026-05.md — that's the historical refactor log, not a current pattern guide. Use this handbook + the ADRs.