System architecture
System architecture
Section titled “System architecture”Audience: backend, devops, leadership
Status: specced
Owns: backend
Depends on: Backend stack ADR, Ecosystem map, Coding standards
Service boundaries, sync/async communication, gateway, external providers, and infra topology. Product language for CAP stays in CAP overview; stack choices live in the ADR.
Four services
Section titled “Four services”| Service | Responsibility | Own DB |
|---|---|---|
| Orchestrator (OL) | Auth, notifications, identity verification, payment, storage — shared by all apps | Yes |
| CAP | Assessment & certification (RPL + NSQ) | Yes |
| LMS | Artisan training / courses | Yes |
| WorkMasters | Portfolio / proof of work for employers | Yes |
All four: Node.js / Express, Prisma migrations, shared kit @yourorg/common, gRPC @yourorg/proto. LMS contracts are specced; WorkMasters remains a product stub. Reserve service shells + DB + CI so the pattern stays consistent.
flowchart TB GW[API Gateway] OL[Orchestrator] CAP[CAP] LMS[LMS] WM[WorkMasters] RMQ[RabbitMQ] SigNoz[SigNoz OTel] GW -->|/v1/ol| OL GW -->|/v1/cap| CAP GW -->|/v1/lms| LMS CAP <-->|"gRPC identity and payment"| OL LMS <-->|gRPC pay| OL CAP <-->|gRPC recs| LMS OL -->|outbox events| RMQ CAP -->|outbox events| RMQ RMQ --> CAP RMQ --> OL OL -.-> SigNoz CAP -.-> SigNoz LMS -.-> SigNoz WM -.-> SigNozSync vs async
Section titled “Sync vs async”| Pattern | When | Transport |
|---|---|---|
| gRPC | Caller needs an answer now (identity verify, account provision, payment initiate) | .proto in @yourorg/proto |
| RabbitMQ | Other services should react; caller does not wait (user created, user deleted, payment succeeded, application certified) | Topic / routing keys + Event catalog |
Transactional outbox
Section titled “Transactional outbox”RabbitMQ alone cannot atomically pair “DB write + publish.” Pattern:
- In the same DB transaction as the state change, insert into
outbox_events(type, payload,created_at,published_atnull). - A lightweight poller publishes unpublished rows to RabbitMQ, then sets
published_at. - Consumers are idempotent (dedupe by event id) — delivery is at-least-once.
Simple poller is enough at current scale; Debezium/CDC is a later option if polling load becomes an issue.
Auth / SSO
Section titled “Auth / SSO”- Orchestrator issues JWT access (short-lived) + refresh tokens.
- Other services verify JWT locally via JWKS — no gRPC to OL on every request.
- Register accepts an
intents[]array (e.g.cap,lms) so OL can emit provisioning events / make gRPC calls for the relevant platforms. - CAP dual-hat: after login, clients call
GET /me(and/me/profilewithout acting-centre headers). Staff invite uses gRPCProvisionAccount(password mail in-process). Account delete is OLPOST /auth/delete-account→user.deleted.
Payment (pricing vs processing)
Section titled “Payment (pricing vs processing)”Orchestrator processes payments but never prices them. CAP (or LMS later) owns amount calculation and the client-facing pay endpoint; CAP calls Orchestrator over gRPC with (amount, referenceType, referenceId). Provider webhooks hit Orchestrator only. Full sequence: Payment architecture.
Gateway & staging
Section titled “Gateway & staging”| Environment | Notes |
|---|---|
| Staging | www.staging-api.elimi-ecosystem.e-limi.africa — health routes live for OL and related services |
| Production | api.elimi-ecosystem.e-limi.africa |
| Paths | /v1/ol/…, /v1/cap/…, /v1/lms/… (gateway routing for available services; nginx samples in monorepo infra/deploy/nginx/) |
Gateway owns a single public entry, rate limiting, and can centralize JWT verification at the edge; services still validate JWT locally for defense in depth.
External services
Section titled “External services”| Concern | Provider (current) | Ownership |
|---|---|---|
| Storage | Cloudinary (or multi-provider later) | Centralized in Orchestrator — signed direct-upload URLs; avoids duplicating credentials across four codebases |
| Identity | NIN (extensible later) | Orchestrator identity verification service |
| Payment | Paystack (provider-flexible adapters) | Processing in Orchestrator; pricing in CAP (or LMS later). Client pays via CAP HTTP → CAP gRPC InitiatePayment → OL; webhook only on OL; CAP unlocks on payment.completed. See Payment architecture. |
| Backup | pgBackRest or WAL-G — continuous WAL + nightly base backups to S3-compatible storage (B2 / Spaces / R2) |
DevOps; schedule restore drills |
Data pipeline & audit (outline)
Section titled “Data pipeline & audit (outline)”Two different “audit” concerns:
| Kind | Where |
|---|---|
| Domain history | CAP-local: stage_history, feedback, application_versions, interview evaluations — not a separate service |
| Security / access audit | Dedicated Audit consumer later — async via RabbitMQ only (never on the critical path). Postgres append-only, range-partitioned by month |
Analytics is a separate pipeline: consume domain events → ETL into aggregation store (materialized views or a second ClickHouse instance). Do not overload the Audit service.
LMS / WorkMasters
Section titled “LMS / WorkMasters”- LMS: specced — overview, data model,
lms-openapi.yaml. Tables includelms_users, onboarding, courses/modules/items, entitlements, enrollments, progress, attempts, SCORM sessions, LMS certificates. - WorkMasters: still a stub —
portfolios,portfolio_items(link CAP / later LMS certificates), optionalemployer_views/ leads
See LMS, WorkMasters.
Related runbooks
Section titled “Related runbooks”- Observability — metrics, logs, traces, SLOs
- CI/CD — GitHub Actions, PM2 role, migrations
- Console commands — deploy-time and manual CLI backfills (CAP + Orchestrator)
- Payment architecture — pricing in CAP; processing in OL
- Event catalog — fill before CAP workflow implementation
- RBAC matrix — fill before authz middleware
- Architecture handover — design SoT index