OpenResidency Docs

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, no express, no @prisma — verified: zero framework imports in src/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 smoke needs 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 projectHexagonalCleanClassic layered
Layer 1 (controllers, guards)Inbound / driving adaptersInterface adaptersPresentation
Layer 2 (core)The hexagon (app + domain)Use cases + entitiesApplication + domain
Layer 3 + adaptersOutbound / driven ports + adaptersInterface adaptersInfrastructure

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.

On this page