SPI & Seam Architecture (as-built)
This document is the as-built (IST) view of the asr-service: what the code actually wires today, with the focus the architecture board asked for — the SPIs (Service Provider Interfaces) and the seams the system is being grown along. It is the developer-facing companion to the aspirational (SOLL) Building Block View: where that document describes the target system, this one describes the running one and marks, per element, whether it is implemented, stubbed, or shape-only (interface present, no driver yet).
Scope. Only the seam-bearing parts of the service. The full data model is in ASR Data Model; the target component tree is in Building Blocks. Runtime flows that are not about a seam (VAD issuance, M2M) live in their own building-block docs.
How to keep this document alive
Section titled “How to keep this document alive”This is a living document. Each diagram below carries a <!-- derived-from --> comment naming the source files and ADRs it reflects. The contract is:
- A ticket that changes a seam changes its diagram in the same PR. If you add an SPI, add an implementor, add a lifecycle verb, or move the seam from packages to Quarkus extensions (ADR-00010 phase 2), update the matching diagram here. The
derived-fromfront-matter is the watch-list — a PR touching any listed path should touch this file. - Status markers are part of the diagram. When a
stubbecomes real or ashape-onlyinterface gets its first implementor, flip the marker and the legend note. The markers are the difference between this doc and the SOLL one. - The glossary is the authority for names. Terms used here (Claim Verification Provider, Verified Claim Set, Attestation, Lifecycle verb, Enrichment, Intake Country, Actor) are defined in
docs/ai/CONTEXT.mdand the arc42 glossary. Don’t redefine them here.
Legend used throughout:
| Marker | Meaning |
|---|---|
| ✅ implemented | code exists and is wired into a runtime path |
| 🟡 stubbed | wired, but the external edge is faked (e.g. broker claims come from a test source, not a live signed token) |
| ⚪ shape-only | interface defined, no implementor yet — a marked seam |
| 🔒 core-only | exists in the pure core but has no driver (endpoint / scheduler) wired |
1. System context (C4 Level 1)
Section titled “1. System context (C4 Level 1)”Who and what the seam-bearing service talks to. The two external seams — Keycloak-as-broker for identity, and register sources for claims — are where the SPIs sit.
graph TB
applicant["👤 Applicant / Requestor<br/>(same person, two phases)"]
admin["👤 Admin<br/>(reviewer)"]
sysacct["⚙️ Expiry service account<br/>🔒 no driver yet"]
consumer["🤖 M2M Consumer"]
subgraph asr["asr-service (Quarkus, Java 25)"]
core["Core + SPI<br/>org.bdinetwork.asr"]
end
kc["🔑 Keycloak<br/>IdP + broker"]
eh["eHerkenning broker<br/>(Signicat OIDC facade)<br/>🟡 simulated realm"]
reg["Register sources<br/>KvK / VIES / GLEIF / KBO<br/>⚪ not yet called"]
obj["Object store<br/>MinIO / S3<br/>⚪ future — Postgres today"]
pg[("PostgreSQL")]
applicant -->|"intake (anonymous), edit, step-up"| core
admin -->|"review verbs"| core
sysacct -.->|"expire sweep 🔒"| core
consumer -->|"VAD via client-credentials"| core
core -->|"persists (incl. document bytes today)"| pg
core -->|"OIDC resource server + admin client"| kc
kc -.->|"federates (first-broker-login)"| eh
core -.->|"future backend lookups"| reg
core -.->|"future document bytes via ObjectStorageProvider"| obj
classDef stub stroke-dasharray: 5 5;
class eh,reg,obj,sysacct stub;
Read this as: the applicant and admin drive the service directly; the service never talks to eHerkenning — Keycloak brokers it (ADR-00008), and ASR only ever consumes a broker-neutral result. Register sources are a declared future seam (the BackendEvidenceVerificationProvider, §3) with no caller yet. An external object store is likewise a declared future seam (the ObjectStorageProvider, §3, ADR-00021) — document bytes live in PostgreSQL today.
2. Layered packages & the SPI seam (C4 Level 2)
Section titled “2. Layered packages & the SPI seam (C4 Level 2)”The single deployable is split into three package layers (ADR-00009, ADR-00010 phase 1). The SPI package is the seam: the core defines the interfaces, and the marketplace and consortium layers plug in implementations discovered via CDI. The core never imports downward.
graph TB
subgraph ctn["nl.ctn.asr — consortium (CTN-specific)"]
ctnAssoc["CtnAssociationProvider ✅"]
ctnCountry["CtnCountryOnboardingProvider<br/>(abstract skeleton)<br/>+ NL / BE / DE subclasses ✅"]
ctnNlPol["CtnNlOnboardingEnrichmentPolicy ✅"]
end
subgraph mkt["org.bdinetwork.marketplace.asr — reusable implementations"]
ehProv["EHerkenningClaimVerificationProvider 🟡"]
exAssoc["ExampleMarketplaceAssociationProvider ✅"]
end
subgraph coreL["org.bdinetwork.asr — core"]
subgraph spi["⟦ SPI seam ⟧ org.bdinetwork.asr.spi"]
iOnb["OnboardingProvider"]
iCb["OnboardingCallback"]
iPol["OnboardingEnrichmentPolicy"]
iCvp["EvidenceVerificationProvider<br/>(Interactive / Backend)"]
iAssoc["AssociationProvider"]
iOsp["ObjectStorageProvider"]
end
svc["OnboardingRequestService<br/>(implements OnboardingCallback) ✅"]
shell["OnboardingTransitions<br/>(transition shell) ✅"]
lc["OnboardingLifecycle<br/>(pure core) ✅"]
defPol["PermissiveOnboardingEnrichmentPolicy<br/>(default) ✅"]
docSvc["DocumentService<br/>(store boundary) 🔒 no HTTP driver"]
pgOsp["PostgresObjectStorageProvider ✅"]
repo["Repositories + JPA model ✅"]
end
ctnCountry -.implements.-> iOnb
ctnNlPol -.implements.-> iPol
ctnAssoc -.implements.-> iAssoc
exAssoc -.implements.-> iAssoc
ehProv -.implements.-> iCvp
svc -.implements.-> iCb
defPol -.implements.-> iPol
pgOsp -.implements.-> iOsp
shell -->|"CDI: @All List"| iPol
shell --> ehProv
shell --> lc
svc --> repo
docSvc --> iOsp
docSvc --> repo
classDef seam fill:#fff3cd,stroke:#d39e00;
class iOnb,iCb,iPol,iCvp,iAssoc,iOsp seam;
The dependency rule: arrows of implements point up into the core’s SPI package; the core depends only on its own interfaces. The three CTN country wizards (NL/BE/DE) now share an abstract skeleton, CtnCountryOnboardingProvider — country dispatch, callback handling and the intake sequence are identical; only the country code, wizard route, intake DTO and JAX-RS/OpenAPI surface differ and stay on the concrete subclasses. Swapping eHerkenning for a real broker, or adding a new consortium, is an additive change in the upper layers — the core does not move. ADR-00010 phase 2 (turning these layers into Quarkus extensions / separate Maven modules) would harden this seam from a package boundary into a module boundary; today it is package-only.
3. SPI catalogue (C4 Level 3)
Section titled “3. SPI catalogue (C4 Level 3)”The contracts in org.bdinetwork.asr.spi, their sub-interfaces, value types, and who implements each. This is the heart of the “specific attention to SPIs” request.
classDiagram
direction TB
class OnboardingProvider {
<<SPI>>
+supports(countryCode) boolean
+start(OnboardingCallback) String
}
class OnboardingCallback {
<<SPI core-side>>
+save(CreateOnboardingReq) OnboardingReqResp
+onboardingSucceeded(...) Response
+onboardingFailed() Response
}
class OnboardingEnrichmentPolicy {
<<SPI>>
+supports(country) boolean
+isComplete(OnboardingRequest) boolean
}
class AssociationProvider {
<<SPI>>
+getLegalName() String
+getFriendlyName() String
}
class ObjectStorageProvider {
<<SPI>>
+put(UUID, InputStream) void
+get(UUID) InputStream
}
class EmailProvider {
<<SPI>>
+send(EmailRequest) void
}
class EvidenceVerificationProvider {
<<SPI base>>
+validationSource() String
}
class InteractiveEvidenceVerificationProvider~C~ {
<<SPI>>
+start(AttributeWithEvidence) String
+complete(AttributeWithEvidence, C) EvidenceOutcome
}
class BackendEvidenceVerificationProvider {
<<SPI shape-only>>
+verify(AttributeWithEvidence) Object
}
class EvidenceDetailsSPI {
<<SPI>>
+supports(Evidence.Type) boolean
+isControlAttestation(Evidence) boolean
+getDetails(Evidence) Map
+getAssuranceLevel(Evidence) String
}
EvidenceVerificationProvider <|-- InteractiveEvidenceVerificationProvider
EvidenceVerificationProvider <|-- BackendEvidenceVerificationProvider
class AttributeWithEvidence {
<<interface / subject>>
+getEvidences() List~Evidence~
+addEvidence(Evidence) void
+getValidationStatus() Status
}
class AssuranceLevel {
<<enum>>
LOW
SUBSTANTIAL
HIGH
}
class EvidenceOutcome {
<<sealed>>
Matched
Mismatched
Abandoned
}
InteractiveEvidenceVerificationProvider ..> AttributeWithEvidence
InteractiveEvidenceVerificationProvider ..> EvidenceOutcome
BackendEvidenceVerificationProvider ..> AttributeWithEvidence
EvidenceDetailsSPI ..> AssuranceLevel
SPI contracts (source)
Section titled “SPI contracts (source)”The interfaces and value types, all in org.bdinetwork.asr.spi: OnboardingProvider · OnboardingCallback · OnboardingEnrichmentPolicy · AssociationProvider · ObjectStorageProvider · EvidenceVerificationProvider (Interactive / Backend) · EvidenceDetailsSPI · AssuranceLevel · EvidenceOutcome. The interactive/backend subject is the model interface AttributeWithEvidence (implemented by LegalEntityIdentifier), and the produced record is an Evidence — the former VerifiedClaimSet/VerifiedClaim value record was retired when claim verification became evidence verification (the evidence module carve, ADR-00022 / TDR-0006).
Implementor & status table
Section titled “Implementor & status table”Why two sub-interfaces (ADR-00014): InteractiveEvidenceVerificationProvider is start → redirect, complete → outcome (user present, multi-round) and yields a broker-neutral EvidenceOutcome (Matched / Mismatched / Abandoned); BackendEvidenceVerificationProvider is verify → … (user absent, one synchronous call). Forcing one signature onto both would leak one mode’s accident into the other. The recorded result of a verification is an Evidence row (the evidence module, §6); marketplace/country satellites attach their specialised payload via a @OneToOne and expose it through the EvidenceDetailsSPI (EHerkenningEvidence, DocumentEvidence). The planned nl.ctn.asr.enrichment.* design (Identifier Enrichment Architecture, KvK/VIES/GLEIF) is the backend provider in embryo — folding it behind this seam is deferred convergence debt.
Why ObjectStorageProvider (ADR-00021, Phase 1): document bytes are owned by the seam (put/get on InputStream, keyed by the Document’s UUID), not by the Document metadata — so the Postgres-first PostgresObjectStorageProvider (a document_blob table) can later be swapped for MinIO/S3 without touching the model or any referrer. The contract is InputStream in/out (never byte[]) and offers no presigned/direct-URL operation — document access stays API-mediated. The store boundary DocumentService (ingest guards, per-type expiry, write-once, authorization via an explicit CallerContext) is HTTP-driver-less today (🔒) — callers are tests until the Phase 2 endpoint lands.
4. Seam: onboarding-request lifecycle (ADR-00012)
Section titled “4. Seam: onboarding-request lifecycle (ADR-00012)”The OnboardingRequest status changes only through guarded lifecycle verbs (ADR-00012). The seam is split three ways so that an illegal transition is unrepresentable even from inside the module. Since the onboarding module was zoned to the seam pattern (ADR-00022, §6), the pure core lives in onboarding.internal, the entity in the published onboarding.entity zone, and the CDI shell interface at the module root.
stateDiagram-v2
[*] --> Submitted: Intake (actor-less, not a verb)
Submitted --> UnderReview: startReview ✅<br/>(admin)
UnderReview --> ChangesRequested: sendBack ✅<br/>(admin, note req.)
ChangesRequested --> UnderReview: resubmit ✅<br/>(requestor, enrichment-guarded)
UnderReview --> Approved: approve ✅<br/>(admin)
UnderReview --> Rejected: reject ✅<br/>(admin, note req.)
Submitted --> Rejected: expire 🔒<br/>(system, no driver)
ChangesRequested --> Rejected: expire 🔒<br/>(system, no driver)
Approved --> [*]
Rejected --> [*]
note right of ChangesRequested
Open states = {Submitted, UnderReview, ChangesRequested}
Terminal = {Approved, Rejected}
Both derived from the edge topology, never hardcoded.
end note
The three-part seam — who is allowed to write status
Section titled “The three-part seam — who is allowed to write status”graph LR
res["OnboardingRequestResource<br/>(coarse @RolesAllowed) ✅"]
shell["OnboardingTransitions<br/>(CDI shell — only writer) ✅"]
entity["OnboardingRequest.transitionTo()<br/>(locked setter) ✅"]
core["OnboardingLifecycle<br/>(pure topology table) ✅"]
pol["OnboardingEnrichmentPolicy<br/>(resubmit guard, by Intake Country)"]
res -->|"verb + Actor"| shell
shell -->|"ownership / clock / completeness guards"| shell
shell -->|"resolves by intakeCountry"| pol
shell -->|"transitionTo(target)"| entity
entity -->|"is this edge legal?"| core
core -.->|"absent edge → IllegalTransitionException"| entity
| Layer | Owns | Lives in |
|---|---|---|
Pure core OnboardingLifecycle | the legal (from, verb) → to edge table; isOpen / isTerminal derived from it | onboarding.internal — no JPA/CDI/IO, unit-test target |
Entity OnboardingRequest | a single non-public status + locked transitionTo() delegating legality to the core | onboarding.entity (published zone) — JPA |
CDI shell OnboardingTransitions (impl in internal) | the only writer of status; contextual guards (ownership, clock, per-country completeness), actor/time stamping, persistence | root seam interface + @ApplicationScoped @Transactional impl |
Two-layer authorization: coarse role checks (admin vs requestor) at the HTTP resource; the data-dependent ownership guard (actor.sub == request.applicantUserId) in the shell, where the aggregate is loaded — so a new endpoint cannot bypass it. The guard runs before the state/existence check (findOwned / enrich / resubmit), so a non-owner gets a uniform 403 — never a 404/409 that would leak whether the request exists or what state it is in (SecurityException not IllegalTransitionException).
Lifecycle notifications — the status-change event fan-out (COMP-23)
Section titled “Lifecycle notifications — the status-change event fan-out (COMP-23)”The audit hook is no longer a no-op: the single transition writer now fires a CDI event on every status change, and hidden observers react. The shell OnboardingTransitionsImpl (each verb) and the intake path in OnboardingRequestServiceImpl publish an OnboardingStatusChangedEvent (request, from, to, actor, note) — from == null marks intake creation. Three @ApplicationScoped observers, all onboarding-internal, consume it; because the event is fired and observed only inside the module, it stays in internal and never widens the seam.
graph LR
intake["OnboardingRequestServiceImpl<br/>(intake: from = null → Submitted) ✅"]
shell["OnboardingTransitionsImpl<br/>(lifecycle verbs) ✅"]
evt(["OnboardingStatusChangedEvent<br/>(CDI event, internal)"])
aud["AuditingOnboardingRequestListener<br/>→ AuditRecorder seam ✅<br/>(REQUIRES_NEW, AFTER_SUCCESS)"]
eml["EmailOnboardingRequestListener<br/>→ EmailProvider SPI ✅<br/>(async, AFTER_SUCCESS)"]
log["LoggingOnboardingRequestListener ✅"]
intake -->|"fire"| evt
shell -->|"fire"| evt
evt --> aud
evt --> eml
evt --> log
The audit and email observers fire after the transaction commits (AFTER_SUCCESS) so nothing is recorded or mailed for a rolled-back transition; auditing runs in its own REQUIRES_NEW transaction, and email is dispatched asynchronously. AuditingOnboardingRequestListener is the concrete audit writer the §4 hook always anticipated — it calls the audit module seam (AuditRecorder, §6), the only cross-module edge these listeners add (and one onboarding already had). EmailOnboardingRequestListener renders a per-status template through the EmailProvider SPI (§3, COMP-156).
5. Seam: claim verification (ADR-00014) — eHerkenning step-up
Section titled “5. Seam: claim verification (ADR-00014) — eHerkenning step-up”The interactive seam in motion (ADR-00014; implementation in org.bdinetwork.marketplace.asr.eherkenning): an authenticated requestor proves control of their KvK via eHerkenning, brokered by Keycloak, and the service writes a write-once attestation. The broker-neutral boundary is the key seam — all broker-specific mapping is confined to Keycloak’s IdP-mapper layer.
sequenceDiagram
autonumber
actor R as Requestor (browser)
participant Res as EHerkenningResource<br/>/v1/onboarding-requests/{id}/eherkenning
participant Sh as OnboardingTransitions (shell)
participant KC as Keycloak (broker)
participant EH as eHerkenning broker<br/>🟡 simulated realm
participant P as EHerkenningClaimVerificationProvider
participant V as evidence table<br/>(append-only control evidence)
R->>Res: POST .../eherkenning/start
Res->>Sh: startStepUp(requestId, actor)
Sh->>P: start(subject)
P-->>R: frontend redirect route (data, not origin)
R->>KC: first-broker-login
KC-->>EH: SAML/OIDC federate
EH-->>KC: brokered claims (kvk, loa, acting_subject, represents)
Note over KC: IdP-mapper → user-session-note → protocol-mapper<br/>(claims stay transient — never a user attribute)
KC-->>R: token carrying eHerkenning claims
R->>Res: POST .../eherkenning/token {token}
Res->>Sh: completeStepUpWithToken(requestId, actor, token)
Note over Sh: token verifier is a configured CDI bean<br/>(Path A self-signed) — production trusted-key source deferred (KEYC-1192)
Sh->>P: complete(subject, claims)
P-->>Sh: EvidenceOutcome (Matched / Mismatched / Abandoned)
alt Matched
Sh->>V: record positive control evidence (write-once)
else Mismatched
Sh->>V: record failure evidence + flag for admin
else Abandoned
Note over Sh: nothing persisted — back to edit surface
end
The broker-neutral seam (why it is reusable):
graph LR
subgraph broker["Broker-specific — confined to Keycloak config"]
m["IdP mappers<br/>(literal broker claim names)"]
end
subgraph neutral["Broker-neutral — what ASR code reads"]
t["token: kvk / loa /<br/>acting_subject / represents"]
vcs["EvidenceOutcome + Evidence row<br/>(broker-neutral, eIDAS assurance)"]
end
m -->|"session-note → protocol-mapper"| t
t --> vcs
note["A real-broker swap (Signicat → other)<br/>= a config change in the mapper layer only.<br/>EHerkenningTokenVerifier still reads kvk/loa/acting_subject/represents."]
m -.-> note
The service consumes a broker-neutral EvidenceOutcome (and records a broker-neutral Evidence row) regardless of broker; the only broker-specific knowledge is the literal claim names in the Keycloak IdP-mapper layer (the simulated realm: eherkenning-sim-realm.json). Replacing the simulated realm with a real Herkenningsmakelaar is therefore a configuration change, not a code change (ADR-00014, Path A vs Path B deferred to KEYC-1192).
6. In-process module seams (ADR-00022)
Section titled “6. In-process module seams (ADR-00022)”Alongside the SPI seams (§2–§5, which let outside layers plug implementations in), the service has in-process module seams: boundaries between the business packages under org.bdinetwork.asr themselves, so one module cannot reach another’s internals. Each module exposes a small seam — its root package (service interface + seam value types) plus the whitelisted sub-packages dto, exceptions and the published entity zone — and hides everything else behind internal..; its HTTP rest.. boundary is visible but not injectable by siblings. The rule is enforced by ArchUnit (ModuleBoundaryTest), not the compiler. COMP-146 migrated audit, onboarding and evidence; see ADR-00022 and TDR-0006.
graph TB
subgraph onb["org.bdinetwork.asr.onboarding — module ✅"]
onbSeam["root seam: OnboardingRequestService,<br/>OnboardingTransitions, ApplicantAccountProvisioner<br/>+ dto/ + exceptions/ ✅"]
onbEnt["entity/ OnboardingRequest<br/>(published zone) ✅"]
onbRest["rest/ OnboardingRequestResource,<br/>OnboardingStartResource ✅<br/>(HTTP boundary — not seam)"]
onbInt["internal/ impls, lifecycle,<br/>persistence, keycloak ✅ (hidden)"]
end
subgraph ev["org.bdinetwork.asr.evidence — module ✅"]
evSeam["root seam: EvidenceService, NewEvidence,<br/>EvidenceLifecycleAction + dto/ + exceptions/ ✅"]
evEnt["entity/ Evidence<br/>(published zone) ✅"]
evInt["internal/ EvidenceServiceImpl,<br/>EvidenceLifecycle, persistence ✅ (hidden)"]
end
common["org.bdinetwork.asr.common<br/>Actor ✅"]
model["model/ parking pkg<br/>LegalEntityIdentifier<br/>🔒 dissolve → GitHub #159"]
onbSeam -->|"behavior: drives evidence<br/>with resolved context ✅ enforced acyclic"| evSeam
evEnt -.->|"data: @ManyToOne FK<br/>legal_entity_identifier → onboarding_request"| onbEnt
onbSeam --> common
evSeam --> common
evInt -.->|"data: navigates LEI.onboardingRequest"| onbEnt
onbSeam -.->|"data: reads model.getEvidences() 🔒 deferred cycle"| evEnt
Caller-resolves-ownership (TDR-0006). The EvidenceService seam takes already-resolved context — record(LegalEntityIdentifier, …), listEvidence(LegalEntityIdentifier), evidenceDetailsFor(requestId) — and does no ownership/authorization. Onboarding’s resource/shell resolve the owning request and target identifier first (the two-layer authorization of §4), then call evidence. So evidence imports zero onboarding behavior and the onboarding→evidence behavior edge is one-directional (evidence is a behavior-graph leaf). Actor was promoted to org.bdinetwork.asr.common (#161) so neither module depends on the other just to name the acting identity.
Two graphs, staged enforcement (ADR-00022 Amendment 1). Cross-module dependencies split by target zone into a behavior graph (edges into a module root / dto / exceptions) and a data graph (edges into entity/), each cycle-checked independently. That is what legalises the deliberate two-way onboarding↔evidence relationship: the behavior edge and the data edge point in opposite directions but live in different graphs.
Rule (ModuleBoundaryTest) | Counts | Status |
|---|---|---|
onlySeamReachableAcrossModules | only root + dto + exceptions + entity reachable from outside a module | ✅ enforced (@ArchTest) |
behaviorGraphIsCycleFree | cross-module edges into root / dto / exceptions | ✅ enforced (@ArchTest) |
dataGraphCycleForFutureEnforcement | cross-module edges into entity/ | 🔒 implemented but deferred — real onboarding → evidence → onboarding data-zone cycle routed through the model parking package (model.LegalEntityIdentifier.getEvidences() + EvidenceRepository navigating legalEntityIdentifier.onboardingRequest); held as a non-@ArchTest method + a tripwire test that fails when the cycle is gone. Promote once model dissolves — GitHub #159 |
7. Seam summary
Section titled “7. Seam summary”| Seam | SPI / mechanism | Status | ADR |
|---|---|---|---|
| Country onboarding flow | OnboardingProvider + OnboardingCallback (CDI, supports(country)) | ✅ NL/BE/DE | 00010 |
| Per-country enrichment requirements | OnboardingEnrichmentPolicy (CDI, @All List, by Intake Country) | ✅ default + NL | 00012 |
| Request lifecycle | pure core + locked entity + CDI shell | ✅ verbs; 🔒 expire driver | 00012 |
| Interactive evidence verification | InteractiveEvidenceVerificationProvider<C> → EvidenceOutcome + Evidence | 🟡 eHerkenning, broker stubbed | 00014 |
| Backend evidence verification | BackendEvidenceVerificationProvider | ⚪ shape-only | 00014 |
| Specialised evidence details | EvidenceDetailsSPI (@All List, by Evidence.Type) | ✅ eHerkenning + document | 00022 |
| Document byte storage | ObjectStorageProvider (Postgres-first; UUID-keyed InputStream) | ✅ seam + Postgres impl; 🔒 no HTTP driver | 00021 |
| Broker independence | Keycloak IdP-mapper layer → broker-neutral token | 🟡 simulated realm | 00008, 00014 |
| Identity ownership | Keycloak owns identity; ASR owns verification records | ✅ | 00003 |
| In-process module boundary | package-placement seam + ArchUnit allow-list (§6) | ✅ seam + behavior-graph; 🔒 data-graph deferred (#159) | 00022 |
| Audit | AuditRecorder seam module (COMP-146 pilot) | ✅ seam; ✅ onboarding transition + intake now audited via the status-change event listener (COMP-23) | 00012, 00022 |
| Onboarding lifecycle notifications | CDI OnboardingStatusChangedEvent → audit / email / logging observers (§4) | ✅ audit + logging; ✅ email (SMTP) | 00012 |
| Outbound email | EmailProvider SPI (Qute templates, AFTER_SUCCESS, async) | ✅ SMTP default; ⚪ Azure stub | 00022 |
Related documents
Section titled “Related documents”- Building Block View (SOLL) — target component tree
- ASR Data Model — the
verification/audit_recordtables behind the attestation - ADR-00009, ADR-00010 — the layering the SPI seam sits on
- ADR-00012 — lifecycle transition seam
- ADR-00014 — claim/evidence verification provider seam
- ADR-00021 — document store behind the
ObjectStorageProviderseam - ADR-00022 — in-process module seam pattern (§6)
- TDR-0006 — evidence module carve, caller-resolved ownership, two-graph boundary
docs/ai/CONTEXT.md— glossary for every term used here
In samenwerking met




