Architecture
The core / delivery / ports split — a hexagonal design where the framework-free core is the real asset and the NestJS delivery layer is disposable.
In plain terms. The valuable part of this system is a small piece of logic that does not depend on any particular web technology. Everything around it — the web server, the routes — is replaceable plumbing. The split is deliberate: it is what lets the rules outlive the technology choices made around them.
OpenResidency is a small framework-agnostic core with a thin NestJS delivery layer around it. The split is deliberate, and it's the key to reading the codebase.
The layers
HTTP / OIDC ┌───────────────────────────────────────────────┐
(delivery) │ NestJS controllers · guards · OIDC mount │ Layer 1
└───────────────────────────────────────────────┘
│ calls
Core ┌───────────────────────────────────────────────┐
(no framework)│ foundational · residency · credentials · │ Layer 2
│ oid4vci/vp · sso · operator · consent · audit │
└───────────────────────────────────────────────┘
│ depends on (interfaces)
Ports ┌───────────────────────────────────────────────┐
│ ResidencyStore · ConsentStore · AuditStore … │ Layer 3
└───────────────────────────────────────────────┘
│ │
InMemory* (tests) Prisma* (PostgreSQL)- Layer 1 — delivery / presentation. NestJS controllers, guards (where the actors cross in), and the OIDC provider mount. Thin: it authenticates the caller, translates the request into a plain call on the core, and translates the result back. No business logic.
- Layer 2 — core. All the decision-making. Imports no
@nestjs, noexpress, no@prisma— verified: zero framework imports insrc/core. That's what makes it unit-testable by the smoke scripts (no server) and embeddable as a library. - Layer 3 — ports. Interfaces the core declares for what it can't do itself (persistence).
The core depends on
ResidencyStore, never on Postgres. - Adapters. Two implementations of every port: InMemory (tests / pilots) and Prisma
(production). The same core runs unchanged against either — which is why
npm run smokeneeds no database.
The idea that makes it click: the arrows invert at the ports
Between L1→L2 the controller calls the core. At the ports it flips: the core defines the interface, and the outer Prisma/InMemory adapters implement it. The database depends on the core, not the core on the database. That inversion is why persistence is swappable and the core is testable and portable — a requirement for a Digital Public Good, not just tidiness.
In hexagonal / clean-architecture terms
| This project | Hexagonal | Clean | Classic layered |
|---|---|---|---|
| Layer 1 (controllers, guards) | Inbound / driving adapters | Interface adapters | Presentation |
| Layer 2 (core) | The hexagon (app + domain) | Use cases + entities | Application + domain |
| Layer 3 + adapters | Outbound / driven ports + adapters | Interface adapters | Infrastructure |
Note: the "application layer" (use-case orchestration, e.g. ResidencyService.issue) lives
inside the core here — this codebase doesn't split application and domain into separate layers.
Reading it: a request trace
POST /residency/issue → L1 residency.controller (guard checks the registrar, delegates)
→ L2 ResidencyService.issue (verify → bind → residence → mint → sign) → when it persists,
it calls the L3 ResidencyStore port → at runtime the Prisma adapter → Postgres. Swap in
the InMemory adapter and the same flow runs in a test with no DB.
The takeaway
Delivery is disposable and framework-specific; the core is the real asset and is
framework-free; the ports are the seam that keeps them apart. If you ever dropped NestJS or
swapped Postgres, only the top and bottom layers change — the core doesn't move. When you add
logic, put the decision in src/core behind a port and keep the controller thin.
Source: docs/ARCHITECTURE.md,
src/core,
src/prisma/prisma.service.ts.
The trust flow
The end-to-end lifecycle — deployer stands up the platform, an operator enrols a citizen, a credential is issued, the citizen signs in, and consent and audit record it.
Credentials & trust
Trust artifacts, offline verification, the two verifiers, and did:key vs did:web — how a credential is trusted without calling home.