Architecture
Registry is two deployables — a reactive Kotlin backend and an Angular SPA — talking over a versioned REST API, with an external OIDC provider and a single PostgreSQL database behind the backend. This page describes each service's internal shape and how a request flows end to end.
Backend — hexagonal, reactive
The backend follows Hexagonal architecture (ports & adapters). The domain sits at the centre and knows nothing about HTTP, the database, or the identity provider; everything framework-specific is an adapter at the edge. The dependency rules are not a convention — they are verified at build time by an ArchUnit test (ADR 001).
Layers
domain/— the core:model/(business models,RegistryException, pagination),port/(driven-port interfaces the domain depends on),service/+service/impl/(use-cases), plusvalidator/,annotation/,enumeration/,constant/,handler/and reactiveextension/helpers. No Spring-web or persistence types leak in.infrastructure/out/— the REST API (the outward-facing, "driving" side). Controllers are contract-first: REST semantics — path mapping,@PreAuthorize, OpenAPI tags, bean validation,@AuthenticationPrincipal CurrentUserModel— live on an interface (I…V1Controller); the@RestControllerimpl only delegates to a domain service. Inbound writer DTOs and outbound reader DTOs are mapped to/from domain models.infrastructure/in/— the driven adapters that feed the domain:postgres/(R2DBC repositories implementing the domain ports, entities, entity mappers) andkeycloak/(the OIDC provider adapter).config/— Spring wiring only (SecurityConfig,R2dbcConfig,I18nConfig,GsonConfig,SwaggerConfig,LoggerConfig).
Naming caveat. This codebase inverts the textbook hexagonal labels:
infrastructure/inis the persistence/identity side ("data coming in") andinfrastructure/outis the REST API ("facing out"). This is the opposite of the usual "inbound = REST" convention — see ADR 007.
Reactive from top to bottom
Every layer is non-blocking: WebFlux controllers return Mono/Flux, repositories use the reactive R2DBC driver, and writes run inside reactive transactions (TransactionalOperator) with programmatic R2DBC auditing. Schema management is the one exception — Flyway migrates the database over a JDBC connection on boot, so the backend opens two connections to the same database: an R2DBC pool for runtime and JDBC for migrations (ADR 002). Repositories hand-write SQL via Spring Data @Query, composing reusable fragments (join clauses, visibility/project filters) to stay DRY, and assemble PageModel results from a count + a page query.
Cross-cutting concerns
- Errors — a
RegistryControllerAdvicemapsRegistryException, validation failures, and framework exceptions to a localizedErrorDto(status, code, i18n title/message). - Validation — Jakarta Bean Validation plus a family of custom cross-field constraints (
@StartBeforeEnd,@MinUpperMax,@MovementReason,@BothCannotBeDefined,@AtLeastOneIsDefined,@MovementGuestContent,@DateDefinedForTime,@ProjectOptionDependencies,@ProfileAcceptOrReject), evaluated by a reflection-basedGenericValidator. - i18n — request locale from the
Accept-Languageheader drivesmessages/errorsbundles (English default, French supported). - Pagination —
PageableModel(offset, limit)in,PageModelout; page size is bounded (1–200). - Observability — Spring Actuator with a Micrometer/Prometheus endpoint, feature-flagged per environment.
Frontend — standalone Angular, per-route state
The frontend is a standalone-component Angular 22 SPA (no NgModule), organized by domain.
- Bootstrap & runtime config. Before the app bootstraps,
AppConfig.load()fetches two JSON files —settings/config.json(theme tokens, logos, enabled UI actions, languages, notification durations) andsettings/env.json(backend URL, production flag, no-auth paths). The repo ships only placeholders; the real files are injected per environment, so one built image serves any environment (ADR 008). - Folder taxonomy.
domains/project/**anddomains/user/**hold the feature domains, each with its owndata/(NGXS state, DTOs, models).shell/holds the app frame (navbar, auth-callback).shared/util-*libraries provide UI components, models/enums, the central state, tools, config, and authentication. - State management. NGXS (
@State/@Action/@Selector) exposed to components as Signals through facade classes. Feature state is provided per route — scoped to the lazy-loaded subtree and code-split with it — while a root state holds the current user, preferences (theme, language), and cross-cutting UI (notifications, online status, screen width) (ADR 009). The "active project" is simplycurrentUser.preferences.selectedProfile.project; selecting a profile updates preferences server-side and resets dependent feature states. - HTTP & auth. A functional interceptor attaches the bearer token, substitutes URL placeholders (current user id, selected project id), refreshes the token on
401, and normalizes transport errors into user-facing notifications. - Routing & guards. Lazy
loadChildrenforprojectsandusersbehind anauthGuard; nested guards enforce that a profile is selected (selectedProfileGuard) and that the project has the relevant option enabled (activity,vehicle,alertoption guards) — mirroring the backend's per-project option authorities.
Request flow, end to end
The Security page details the JWT-to-user mapping and how the project-scoped permission check happens on every call.
Deployment topology
Four cooperating tiers:
- PostgreSQL — the single source of truth; schema owned by Flyway.
- OIDC provider — issues and signs JWTs; configured generically (see ADR 004).
- Backend — distroless Java 25 image, non-root, listening on
:8081, exposing/api/v2/**, Prometheus metrics, and (optionally) OpenAPI. CORS is restricted to a configured allowlist. - Frontend — the static bundle served by an unprivileged nginx on
:8080with SPA fallback and hardening headers; parameterized per environment by the two runtime JSON files.
Both services are versioned by semantic-release and published as container images with a retain-5 policy (ADR 010).