Skip to content

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.

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 -.-> SigNoz
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

RabbitMQ alone cannot atomically pair “DB write + publish.” Pattern:

  1. In the same DB transaction as the state change, insert into outbox_events (type, payload, created_at, published_at null).
  2. A lightweight poller publishes unpublished rows to RabbitMQ, then sets published_at.
  3. 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.

  • 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/profile without acting-centre headers). Staff invite uses gRPC ProvisionAccount (password mail in-process). Account delete is OL POST /auth/delete-accountuser.deleted.

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.

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.

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

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: specced — overview, data model, lms-openapi.yaml. Tables include lms_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), optional employer_views / leads

See LMS, WorkMasters.