Skip to content
Deze documentatie wordt actief uitgebreid — kom regelmatig terug.

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.

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-from front-matter is the watch-list — a PR touching any listed path should touch this file.
  • Status markers are part of the diagram. When a stub becomes real or a shape-only interface 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.md and the arc42 glossary. Don’t redefine them here.

Legend used throughout:

MarkerMeaning
✅ implementedcode exists and is wired into a runtime path
🟡 stubbedwired, but the external edge is faked (e.g. broker claims come from a test source, not a live signed token)
⚪ shape-onlyinterface defined, no implementor yet — a marked seam
🔒 core-onlyexists in the pure core but has no driver (endpoint / scheduler) wired

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.


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

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

SPISub-interfaceImplementor(s)LayerStatus
OnboardingProviderCtnCountryOnboardingProvider (abstract skeleton) ← CtnNlOnboardingProvider, CtnBeOnboardingProvider, CtnDeOnboardingProvidernl.ctn
OnboardingCallbackOnboardingRequestServicecore
OnboardingEnrichmentPolicyPermissiveOnboardingEnrichmentPolicy (default), CtnNlOnboardingEnrichmentPolicycore / nl.ctn
AssociationProviderCtnAssociationProvider, ExampleMarketplaceAssociationProvidernl.ctn / marketplace
ObjectStorageProviderPostgresObjectStorageProvidercore✅ seam wired; document feature 🔒 (no HTTP driver)
EvidenceVerificationProviderInteractiveEvidenceVerificationProvider<C>EHerkenningClaimVerificationProvidermarketplace🟡 broker stubbed
EvidenceVerificationProviderBackendEvidenceVerificationProvider⚪ shape-only
EvidenceDetailsSPIEHerkenningEvidenceDetailsProvider, DocumentEvidenceDetailsProvidermarketplace
EmailProviderSmtpEmailProvider (@DefaultBean, Quarkus Mailer + Qute), AzureCommunicationEmailProvider (@IfBuildProperty — throws)core (email)✅ SMTP default; ⚪ Azure stub

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
LayerOwnsLives in
Pure core OnboardingLifecyclethe legal (from, verb) → to edge table; isOpen / isTerminal derived from itonboarding.internal — no JPA/CDI/IO, unit-test target
Entity OnboardingRequesta single non-public status + locked transitionTo() delegating legality to the coreonboarding.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, persistenceroot 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).


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 contextrecord(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)CountsStatus
onlySeamReachableAcrossModulesonly root + dto + exceptions + entity reachable from outside a module✅ enforced (@ArchTest)
behaviorGraphIsCycleFreecross-module edges into root / dto / exceptions✅ enforced (@ArchTest)
dataGraphCycleForFutureEnforcementcross-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

SeamSPI / mechanismStatusADR
Country onboarding flowOnboardingProvider + OnboardingCallback (CDI, supports(country))✅ NL/BE/DE00010
Per-country enrichment requirementsOnboardingEnrichmentPolicy (CDI, @All List, by Intake Country)✅ default + NL00012
Request lifecyclepure core + locked entity + CDI shell✅ verbs; 🔒 expire driver00012
Interactive evidence verificationInteractiveEvidenceVerificationProvider<C>EvidenceOutcome + Evidence🟡 eHerkenning, broker stubbed00014
Backend evidence verificationBackendEvidenceVerificationProvider⚪ shape-only00014
Specialised evidence detailsEvidenceDetailsSPI (@All List, by Evidence.Type)✅ eHerkenning + document00022
Document byte storageObjectStorageProvider (Postgres-first; UUID-keyed InputStream)✅ seam + Postgres impl; 🔒 no HTTP driver00021
Broker independenceKeycloak IdP-mapper layer → broker-neutral token🟡 simulated realm00008, 00014
Identity ownershipKeycloak owns identity; ASR owns verification records00003
In-process module boundarypackage-placement seam + ArchUnit allow-list (§6)✅ seam + behavior-graph; 🔒 data-graph deferred (#159)00022
AuditAuditRecorder seam module (COMP-146 pilot)✅ seam; ✅ onboarding transition + intake now audited via the status-change event listener (COMP-23)00012, 00022
Onboarding lifecycle notificationsCDI OnboardingStatusChangedEvent → audit / email / logging observers (§4)✅ audit + logging; ✅ email (SMTP)00012
Outbound emailEmailProvider SPI (Qute templates, AFTER_SUCCESS, async)✅ SMTP default; ⚪ Azure stub00022


↑ Back to Index | Building Blocks →

In samenwerking met

Connected Trade NetworkConclusionData in LogisticsContargoInland Terminals GroupVan Berkel