Skip to main content

DemozPay — Developer Onboarding Guide

Audience: interns, junior devs, new engineers. Pair this with docs/architecture/SYSTEM_OVERVIEW.md (what the platform is) and docs/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:

ToolRequiredHow to check
Node.js22.13+node -v (pnpm@11 refuses lower; you'll see requires at least Node.js v22.13)
pnpm11.xcorepack enable && corepack prepare pnpm@11.4.0 --activate
Docker + docker-composelatestdocker --version && docker compose version
Postgres client psqlfor ledger/gateway migrationspsql --version
Go1.23+go version (or use the bundled toolchain at .tools/go/bin/go)
bufoptional (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):

ServiceURL
apps/apihttp://localhost:3030 (NOT :3000)
ledger gRPClocalhost:50051
ledger HTTPhttp://localhost:50054
integration-gateway gRPClocalhost:50052
integration-gateway HTTPhttp://localhost:50053
bank-sandboxhttp://localhost:8088
Postgres (api)localhost:5440
Postgres (ledger)localhost:5441
Postgres (gateway)localhost:5442
frontendshttp://localhost:4200..4204
Prometheushttp://localhost:9090
Redpanda Consolehttp://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:

VarWhat it doesDefault
DATABASE_URLPrisma DSN for apps/apirequired
OUTBOX_DATABASE_URLDSN for the outbox publisher (BYPASSRLS role)required if publisher enabled
BETTER_AUTH_SECRETsession encryptionrequired
BETTER_AUTH_URLpublic base URL (e.g. http://localhost:3030)required
LEDGER_GRPC_ADDRwhere apps/api dials the ledgerlocalhost:50051
INTEGRATION_GATEWAY_GRPC_ADDRwhere apps/api dials the gatewaylocalhost:50052
LEDGER_PROTO_PATHabsolute path to ledger.proto<cwd>/packages/contracts/grpc/ledger.proto
EMAIL_PROVIDERlogger (dev) / smtp (prod) / httplogger
SMS_PROVIDERlogger (dev) / ethio-telecom / httplogger (refused in prod by apps/api/src/_infra/sms/sms.module.ts boot guard)
GRPC_LEDGER_AUTH_SECRET, GRPC_GATEWAY_AUTH_SECRETTS-side HMAC signing secrets for R1empty disables signing
GRPC_LEDGER_AUTH_CLIENT_ID, GRPC_GATEWAY_AUTH_CLIENT_IDclient identifier in the signed metadataapi
OUTBOX_PUBLISHER_ENABLEDoutbox → Kafka publisherfalse
EQUB_LEDGER_RECONCILE_VIA_GRPCswap equb reconciliation to Go RPC instead of in-process equb tablesfalse

Go services (per their READMEs):

VarServiceNotes
LEDGER_DATABASE_URLledgerrole must NOT have BYPASSRLS
LEDGER_GRPC_ADDRledgerdefault :50051
LEDGER_HTTP_ADDRledgerdefault :50054
GATEWAY_DATABASE_URLgatewayrole must NOT have BYPASSRLS
GATEWAY_ENABLED_PARTNERSgatewaycomma list; default mock
GATEWAY_DASHEN_BASE_URL, GATEWAY_DASHEN_SIGNING_KEYgatewayrequired if dashen is enabled
GRPC_AUTH_CLIENTSbothJSON {clientId: secret}; ≥16-char secrets enforced
GRPC_AUTH_MODEbothdisabled / 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:

  1. Every new financial table must be tenantId-scoped and have ALTER TABLE … FORCE ROW LEVEL SECURITY + a tenant_isolation policy. See apps/api/prisma/migrations/20260526030000_apply_tenant_rls/migration.sql for the canonical shape.
  2. Money columns: BigInt @db.Numeric(20,0) (santim). Never Float, never Decimal(15,2) on new tables.
  3. New migrations must include a verify guard — a DO $$ … RAISE EXCEPTION block 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):

PackageUnit specsIntegration specs
packages/ewa/50
packages/lending/60
packages/kyc/40
packages/sanctions/20
packages/payroll/340 (3 live at apps/api level)
packages/equb/50
apps/api/src/payroll/consumers/82 (payroll-deductions-poller, court-order-auto-submit)
apps/api/src/payroll/111 (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 behind RUN_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:

ADRRule
005Money is NUMERIC(20,0) santim. Never Float. Use @demoz-pay/shared-money.
006Ledger is sole source of money truth. No new balance columns.
007Idempotency-Key required on every money-moving POST.
008Audit + outbox event + state change live in the same DB transaction.
009No DELETE on financial rows. Reversals create new entries.
010TypeScript + Go only.
011No cross-domain imports. Outbox events for cross-domain communication.
013Every 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:

  1. Use @Public() only for unauthenticated routes (health, metrics, public webhooks with HMAC).
  2. Use @RequireOrgRole('admin','owner') for tenant-admin actions.
  3. Use @RequirePlatformAdmin() for cross-tenant operator actions (TOTP enforced).
  4. Money-moving POST: require Idempotency-Key header; thread it to the ledger gRPC.
  5. Add a row to docs/audits/API_INVENTORY_FRESH.md in 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.ts enum.
  • 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 balance column.
  • Add a Float or Decimal money 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 EwaRequest repository + OutboxEvent in same Prisma transaction.
    • Outbox poller ships ewa.requested.v1 to Kafka (if enabled).

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.md and 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:

  1. Read the ADR (docs/adr/).
  2. Read the per-domain README (packages/<domain>/README.md where it exists; services/<svc>/README.md).
  3. Read the runbook (docs/runbooks/ — mostly stubs today; flag missing runbooks).
  4. 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.