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

ASR Data Model

The data model for the Association Register (ASR) — the authoritative store for CTN participants, their legal identities, identifiers, verifications, endpoints, dataspace roles and onboarding state. This document describes the target (SOLL) model: the schema the ASR should converge on, aligned with ADR-00003 — Separate users and participants.

The diagram in this document is large by design. A zoom-friendly, ERD-only companion lives at ctn-asr-data-model-erd.md — use that when you want to pan and zoom the entity relationships without the surrounding prose.

The ASR is not the system of record for identities. Per ADR-00003, a strict separation applies: human and machine identities live in Keycloak; participant business data lives in the ASR database. The two are linked by a single cross-reference — the Keycloak Organization ID on the ASR participant record, and the ASR participant ID carried as the Keycloak Organization alias.

Lives in Keycloak (identity)Lives in ASR (participants)
User credentials, MFA, sessionsParticipant record (legal entity registration)
Minimal user profile (email, name)Business identifiers: KVK / KBO / HRB, EUID, LEI, EORI, VAT
User-to-participant membershipVerification records and their status
Per-participant user roles (admin / member / signatory) via Organization Groups
M2M client credentials (client secret)Dataspace participation roles (data owner / consumer / provider)
Per-participant IdP federationOnboarding workflow state, evidence, audit trail
A reference to each Keycloak M2M client (no secret)

Consequence for the model: the ASR stores no passwords and no client secrets. Where the ASR needs to point at an identity (the participant owner, an application reviewer, a provisioned M2M client) it stores the Keycloak ID as an opaque string reference, not a foreign key — there is no database-level FK across the system boundary.

Every ASR table uses one audit/meta block. No variants. This replaces the inconsistent mix that existed across earlier prototypes (dt_created, dt_modified, created, created_on, createdAt, dt_updated, submitted_at-as-created, mixed created_by types, etc.).

FieldTypeRule
created_attimestamptzNOT NULL DEFAULT now(); never written by the application after insert
updated_attimestamptzNOT NULL DEFAULT now(); maintained by an BEFORE UPDATE trigger
created_byvarchar(100)Keycloak user ID, or system for automated processes
updated_byvarchar(100)Keycloak user ID, or system
is_deletedbooleanNOT NULL DEFAULT false; soft delete — all reads filter is_deleted = false

Exceptions, by design:

  • audit_record is an append-only log: it has created_at only (no updated_at, no is_deleted).
  • Keycloak entities (kc_* in the diagram) are external; they are managed by Keycloak and are not subject to this convention. They appear in the diagram only to show the link points.

Domain status fields (e.g. participant.status, verification.status, endpoint.lifecycle_status) are separate from is_deleted and describe business lifecycle, not record deletion.

Solid lines are intra-ASR foreign keys (|| = mandatory, |o = nullable). Dashed lines (..) are logical cross-system links to Keycloak records — resolved by ID at runtime, with no database foreign key.

erDiagram
    participant ||--o{ legal_entity : "has"
    legal_entity ||--o{ legal_entity_identifier : "has"
    legal_entity ||--o{ contact : "has"
    participant ||--o{ endpoint : "registers"
    participant ||--o{ verification : "verified by"
    participant ||--o{ participant_role : "plays (dataspace roles)"
    participant ||--o{ m2m_client_ref : "owns"
    participant |o--o{ onboarding_request : "results in"

    kc_organization ||--o{ kc_user : "members"
    kc_organization ||--o{ kc_organization_group : "has (admin/signatory)"
    kc_organization ||--o{ kc_m2m_client : "has"

    participant ||..|| kc_organization : "org-id link (no DB FK)"
    m2m_client_ref ||..|| kc_m2m_client : "client-id link (no DB FK)"
    participant }o..o| kc_user : "owner / initial admin (id ref)"
    onboarding_request }o..o| kc_user : "applicant / reviewed_by (id ref)"

    participant {
        uuid participant_id PK
        varchar keycloak_organization_id UK "link to Keycloak Org (no FK)"
        varchar owner_user_id "Keycloak user id (id ref)"
        varchar status "Prospective|Active|Suspended"
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    legal_entity {
        uuid legal_entity_id PK
        uuid participant_id FK
        varchar primary_legal_name
        varchar address_line1
        varchar postal_code
        varchar city
        varchar country_code
        boolean verified
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    legal_entity_identifier {
        uuid identifier_id PK
        uuid legal_entity_id FK
        varchar identifier_type "KVK|KBO|HRB|HRA|EUID|LEI|EORI|VAT|RSIN|SIREN|SIRET|UID|VESTIGINGSNUMMER|PEPPOL"
        varchar identifier_value
        varchar registry_name
        varchar identifier_scope "ENTITY|ESTABLISHMENT"
        varchar validation_status "Pending|Verified|Rejected"
        varchar validation_source "KvK|KBO|VIES|GLEIF|Handelsregister|OffeneRegister|recherche-entreprises|Zefix"
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    contact {
        uuid contact_id PK
        uuid legal_entity_id FK
        varchar contact_type "PRIMARY|BILLING|TECHNICAL"
        varchar full_name
        varchar email
        varchar phone
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    endpoint {
        uuid endpoint_id PK
        uuid participant_id FK
        varchar endpoint_name
        varchar endpoint_url
        varchar data_category
        varchar endpoint_type
        varchar lifecycle_status "Registered|Validated|Suspended|Retired"
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    verification {
        uuid verification_id PK
        uuid participant_id FK
        varchar type "KvkCheck|LeiCheck|VatCheck|DnsVerification|EmailVerification|EHerkenning|OnboardingApproval"
        varchar category "Admin|Automatic|Organization"
        varchar status "Pending|Approved|Rejected|Revoked"
        timestamptz handled_at
        varchar handled_by
        text evidence
        text reason
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    participant_role {
        uuid role_id PK
        uuid participant_id FK
        varchar role "DataOwner|DataConsumer|DataProvider|ServiceProvider"
        date start_date
        date end_date
        varchar loa
        boolean compliancy_verified
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    m2m_client_ref {
        uuid m2m_client_ref_id PK
        uuid participant_id FK
        varchar keycloak_client_id UK "link to Keycloak client (no FK)"
        varchar keycloak_organization_id "link"
        varchar client_name
        text assigned_scopes "array, mirrored from Keycloak"
        varchar status "Active|Suspended|Revoked"
        timestamptz last_rotated_at
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    onboarding_request {
        uuid request_id PK
        uuid participant_id FK "nullable, set on approval"
        varchar applicant_user_id "Keycloak user id (id ref)"
        varchar applicant_email
        varchar applicant_name
        varchar legal_name
        varchar kvk_number
        varchar status "Submitted|UnderReview|Approved|Rejected"
        timestamptz submitted_at
        timestamptz reviewed_at
        varchar reviewed_by "Keycloak user id (id ref)"
        timestamptz created_at
        timestamptz updated_at
        varchar created_by
        varchar updated_by
        boolean is_deleted
    }
    audit_record {
        uuid audit_record_id PK
        varchar entity_type
        varchar entity_key
        varchar action "Added|Modified|Deleted"
        varchar performed_by
        timestamptz created_at
        jsonb payload
    }
    kc_user {
        varchar user_id PK
        varchar email
        varchar given_name
        varchar family_name
    }
    kc_organization {
        varchar org_id PK
        varchar alias "carries participant_id"
        varchar name
    }
    kc_organization_group {
        varchar group_id PK
        varchar org_id FK
        varchar path "/admin, /signatory"
    }
    kc_m2m_client {
        varchar client_id PK
        varchar org_id FK
        varchar client_name
        varchar secret "stored in Keycloak only"
        text scopes
    }
EntityPurposeKey relationships
participantRoot entity: a registered CTN participant. Holds lifecycle status, and the link to its Keycloak Organization.Parent of legal entities, endpoints, verifications, roles, M2M references
legal_entityThe verified legal identity (name, address) behind a participant. Usually 1:1 with participant.FK → participant
legal_entity_identifierBusiness identifiers with validation status and enrichment source. Authoritative here, never in Keycloak. Carries identifier_scope to distinguish entity-level identifiers (KVK, KBO, HRB/HRA, SIREN, UID, EUID, LEI, EORI, VAT, RSIN) from establishment-level ones (vestigingsnummer, SIRET) — see Two-Level Identity Model below.FK → legal_entity
contactBusiness contact persons (primary / billing / technical). Not authentication users — those live in Keycloak.FK → legal_entity
endpointProvider API endpoints with lifecycle status (registered → validated → suspended → retired).FK → participant
verificationOne row per verification check, with status, evidence and handler. Replaces scattered validation_status flags; gives a full auditable trail.FK → participant
participant_roleDataspace participation roles (DataOwner / DataConsumer / DataProvider / ServiceProvider) with validity dates and assurance. Distinct from Keycloak user roles.FK → participant
m2m_client_refASR-side reference to a Keycloak M2M client provisioned for the participant: client name, scopes (mirrored for display/audit), status. No secret.FK → participant; logical link → kc_m2m_client
onboarding_requestIntake/onboarding request; links to the participant once approved.FK → participant (nullable); id-ref → kc_user
audit_recordAppend-only change log across all entities (entity type, key, action, actor, payload).None (standalone)

Two-Level Identity Model (entity vs establishment)

Section titled “Two-Level Identity Model (entity vs establishment)”

(Added 2026-06 — from batch-enrichment learnings; see Identifier Enrichment Architecture.)

Business identifiers live at two scopes that must not be conflated:

  • Entity / rechtspersoon — KVK, HRB/HRA, KBO, SIREN, UID (CHE). The carriers of LEI, RSIN, VAT, EUID. One value per legal entity, regardless of how many locations it has.
  • Establishment / vestiging — a physical location with its own address but no own LEI/VAT, identified by vestigingsnummer (NL), SIRET (FR), or equivalent.

The current model keeps both on legal_entity_identifier, tagged with identifier_scope (ENTITY | ESTABLISHMENT). DE/CH branches are usually not separately numbered (a Zweigniederlassung refers back to the parent HRB/UID); for those, the establishment is distinguished by address (and, in batch enrichment, a geocoded location code). A future refinement could promote establishments to their own establishment table (legal_entity 1 ──< establishment N) if per-location endpoints/roles are needed.

Section titled “Keycloak (external — shown for the link only)”
EntityPurpose
kc_userHuman or service identity. Owns credentials and minimal profile.
kc_organizationOne Keycloak Organization per ASR participant. Its alias carries the ASR participant_id.
kc_organization_groupPer-participant user roles (/admin, /signatory) as Organization Groups.
kc_m2m_clientThe M2M client and its secret — the credential the ASR provisions but never stores.
Section titled “5. M2M Client Credentials and the Keycloak Link”

Client credentials are created and held exclusively in Keycloak. The ASR fronts the lifecycle (create / rotate / revoke) via the Keycloak Admin REST API — see Keycloak M2M Authentication — and keeps only a non-secret reference row (m2m_client_ref) for listing, audit and participant scoping.

The links between the two systems, all by opaque ID (no database FK):

  • participant.keycloak_organization_idkc_organization.org_id (and the reverse via the Organization alias).
  • m2m_client_ref.keycloak_client_idkc_m2m_client.client_id.
  • participant.owner_user_id, onboarding_request.applicant_user_id / reviewed_bykc_user.user_id.

Never stored in the ASR: client secrets, passwords, MFA state, session/refresh tokens, or business identifiers mirrored as Keycloak attributes (an anti-pattern called out in ADR-00003).

A PostgreSQL CREATE TABLE script implementing this model is maintained alongside this document: the SQL DDL shown in full below (jump) (PostgreSQL 15+). It contains the ten ASR tables, the shared updated_at trigger, CHECK constraints for the controlled vocabularies, foreign keys (ON DELETE CASCADE, except onboarding_request which is ON DELETE SET NULL), unique constraints, and indexes — including the partial unique index on participant.keycloak_organization_id and uq_identifier. The Keycloak entities are intentionally absent: they are not created or owned by this database.

This consolidated script is a reference of the target schema. In the implementation repo the schema is applied through Liquibase YAML changesets (TDR-0001, see Database Schema Management — PostgreSQL on Azure); this file is the basis for the baseline changelog, after which every change is its own reviewed changeset rather than an edit to a create-all script.

ctn-asr-schema.sql — full PostgreSQL DDL
-- =============================================================================
-- CTN Association Register (ASR) — PostgreSQL schema (SOLL / target state)
-- Reference DDL for the data model described in ctn-asr-data-model.md
-- Target: PostgreSQL 15+
--
-- Scope: ASR-owned tables only. Human/machine identities and M2M client
-- secrets live in Keycloak (see ADR-00003) and are referenced here by opaque
-- ID string, with NO database foreign key across the system boundary.
--
-- Meta-field convention (every table except audit_record):
-- created_at, updated_at, created_by, updated_by, is_deleted
--
-- NOTE: This is a CONSOLIDATED REFERENCE of the target schema. The ASR applies
-- schema through Flyway versioned migrations (src/main/resources/db/migration/,
-- V<n>__*.sql) per ctn-database-schema-management-azure.md — not via a single
-- create-all script or ORM auto-DDL. Use this file to seed V001__initial_schema.sql.
-- =============================================================================
BEGIN;
-- gen_random_uuid() is built into PostgreSQL 13+.
-- On older servers, enable it explicitly:
-- CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- -----------------------------------------------------------------------------
-- Shared trigger function: maintain updated_at on every UPDATE
-- -----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION asr_set_updated_at()
RETURNS trigger AS $
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
-- -----------------------------------------------------------------------------
-- participant — root entity; links to its Keycloak Organization
-- -----------------------------------------------------------------------------
CREATE TABLE participant (
participant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
keycloak_organization_id varchar(255),
owner_user_id varchar(255),
status varchar(20) NOT NULL DEFAULT 'Prospective'
CONSTRAINT participant_status_check
CHECK (status IN ('Prospective','Active','Suspended','Terminated')),
metadata jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
COMMENT ON COLUMN participant.keycloak_organization_id IS
'Link to Keycloak Organization (opaque ID, no DB FK; see ADR-00003)';
COMMENT ON COLUMN participant.owner_user_id IS
'Keycloak user ID of the initial admin (opaque ID, no DB FK)';
CREATE UNIQUE INDEX uq_participant_kc_org
ON participant (keycloak_organization_id)
WHERE keycloak_organization_id IS NOT NULL;
CREATE INDEX idx_participant_status
ON participant (status) WHERE is_deleted = false;
CREATE TRIGGER trg_participant_updated
BEFORE UPDATE ON participant
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- legal_entity — verified legal identity behind a participant
-- -----------------------------------------------------------------------------
CREATE TABLE legal_entity (
legal_entity_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id uuid NOT NULL
REFERENCES participant (participant_id) ON DELETE CASCADE,
primary_legal_name varchar(255) NOT NULL,
address_line1 varchar(255),
address_line2 varchar(255),
postal_code varchar(20),
city varchar(255),
province varchar(255),
country_code varchar(2),
verified boolean NOT NULL DEFAULT false,
metadata jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
CREATE INDEX idx_legal_entity_participant ON legal_entity (participant_id);
CREATE INDEX idx_legal_entity_name ON legal_entity (primary_legal_name);
CREATE INDEX idx_legal_entity_active ON legal_entity (participant_id) WHERE is_deleted = false;
CREATE TRIGGER trg_legal_entity_updated
BEFORE UPDATE ON legal_entity
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- legal_entity_identifier — business identifiers (authoritative in ASR)
-- -----------------------------------------------------------------------------
CREATE TABLE legal_entity_identifier (
identifier_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
legal_entity_id uuid NOT NULL
REFERENCES legal_entity (legal_entity_id) ON DELETE CASCADE,
identifier_type varchar(20) NOT NULL
CONSTRAINT legal_entity_identifier_type_check
CHECK (identifier_type IN ('KVK','KBO','HRB','EUID','LEI','EORI','VAT','SIREN','CRN')),
identifier_value varchar(100) NOT NULL,
registry_name varchar(255),
registry_url varchar(500),
valid_from timestamptz,
valid_to timestamptz,
validation_status varchar(20) NOT NULL DEFAULT 'Pending'
CONSTRAINT legal_entity_identifier_status_check
CHECK (validation_status IN ('Pending','Verified','Rejected')),
validation_source varchar(20)
CONSTRAINT legal_entity_identifier_source_check
CHECK (validation_source IN ('KvK','KBO','VIES','GLEIF','Manual')),
validation_date timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false,
CONSTRAINT uq_identifier UNIQUE (legal_entity_id, identifier_type, identifier_value)
);
CREATE INDEX idx_identifier_entity ON legal_entity_identifier (legal_entity_id);
CREATE INDEX idx_identifier_value ON legal_entity_identifier (identifier_type, identifier_value);
CREATE TRIGGER trg_identifier_updated
BEFORE UPDATE ON legal_entity_identifier
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- contact — business contacts (NOT authentication users; those live in Keycloak)
-- -----------------------------------------------------------------------------
CREATE TABLE contact (
contact_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
legal_entity_id uuid NOT NULL
REFERENCES legal_entity (legal_entity_id) ON DELETE CASCADE,
contact_type varchar(20) NOT NULL
CONSTRAINT contact_type_check
CHECK (contact_type IN ('PRIMARY','BILLING','TECHNICAL','LEGAL')),
full_name varchar(255) NOT NULL,
first_name varchar(100),
last_name varchar(100),
email varchar(255) NOT NULL,
phone varchar(50),
mobile varchar(50),
job_title varchar(100),
is_primary boolean NOT NULL DEFAULT false,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
COMMENT ON TABLE contact IS
'Business contact persons. Authentication users are managed in Keycloak (ADR-00003).';
CREATE INDEX idx_contact_entity ON contact (legal_entity_id);
CREATE INDEX idx_contact_email ON contact (email);
CREATE TRIGGER trg_contact_updated
BEFORE UPDATE ON contact
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- endpoint — provider API endpoints with lifecycle status
-- -----------------------------------------------------------------------------
CREATE TABLE endpoint (
endpoint_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id uuid NOT NULL
REFERENCES participant (participant_id) ON DELETE CASCADE,
endpoint_name varchar(255) NOT NULL,
endpoint_url varchar(500),
endpoint_description text,
data_category varchar(100),
endpoint_type varchar(50) NOT NULL DEFAULT 'REST_API',
authentication_method varchar(50),
lifecycle_status varchar(20) NOT NULL DEFAULT 'Registered'
CONSTRAINT endpoint_lifecycle_check
CHECK (lifecycle_status IN ('Registered','Validated','Suspended','Retired')),
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
CREATE INDEX idx_endpoint_participant ON endpoint (participant_id);
CREATE INDEX idx_endpoint_status ON endpoint (lifecycle_status) WHERE is_deleted = false;
CREATE TRIGGER trg_endpoint_updated
BEFORE UPDATE ON endpoint
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- verification — one auditable row per verification check (authoritative in ASR)
-- -----------------------------------------------------------------------------
CREATE TABLE verification (
verification_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id uuid NOT NULL
REFERENCES participant (participant_id) ON DELETE CASCADE,
type varchar(40) NOT NULL
CONSTRAINT verification_type_check
CHECK (type IN ('KvkCheck','KboCheck','LeiCheck','VatCheck','DnsVerification',
'EmailVerification','EHerkenning','OnboardingApproval',
'BusinessRegisterExtract','ConditionsOfUseAcceptance')),
category varchar(20) NOT NULL
CONSTRAINT verification_category_check
CHECK (category IN ('Admin','Automatic','Organization')),
status varchar(20) NOT NULL DEFAULT 'Pending'
CONSTRAINT verification_status_check
CHECK (status IN ('Pending','Approved','Rejected','Revoked')),
handled_at timestamptz,
handled_by varchar(100),
evidence text,
reason text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
CREATE INDEX idx_verification_participant ON verification (participant_id);
CREATE INDEX idx_verification_status ON verification (status) WHERE is_deleted = false;
CREATE TRIGGER trg_verification_updated
BEFORE UPDATE ON verification
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- participant_role — dataspace participation roles ONLY
-- (user/auth roles admin/member/signatory live in Keycloak Organization Groups)
-- -----------------------------------------------------------------------------
CREATE TABLE participant_role (
role_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id uuid NOT NULL
REFERENCES participant (participant_id) ON DELETE CASCADE,
role varchar(20) NOT NULL
CONSTRAINT participant_role_check
CHECK (role IN ('DataOwner','DataConsumer','DataProvider','ServiceProvider')),
start_date date,
end_date date,
loa varchar(20),
compliancy_verified boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
COMMENT ON TABLE participant_role IS
'Dataspace participation roles only. User/authentication roles live in Keycloak (ADR-00003).';
CREATE INDEX idx_participant_role_participant ON participant_role (participant_id);
CREATE TRIGGER trg_participant_role_updated
BEFORE UPDATE ON participant_role
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- m2m_client_ref — ASR-side reference to a Keycloak M2M client (NO secret)
-- -----------------------------------------------------------------------------
CREATE TABLE m2m_client_ref (
m2m_client_ref_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id uuid NOT NULL
REFERENCES participant (participant_id) ON DELETE CASCADE,
keycloak_client_id varchar(255) NOT NULL,
keycloak_organization_id varchar(255),
client_name varchar(255) NOT NULL,
assigned_scopes text[] NOT NULL DEFAULT '{}',
status varchar(20) NOT NULL DEFAULT 'Active'
CONSTRAINT m2m_client_ref_status_check
CHECK (status IN ('Active','Suspended','Revoked')),
last_rotated_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false,
CONSTRAINT uq_m2m_client_ref_kc_client UNIQUE (keycloak_client_id)
);
COMMENT ON TABLE m2m_client_ref IS
'Reference to a Keycloak M2M client. The client secret is held ONLY in Keycloak (ADR-00003); never stored here.';
COMMENT ON COLUMN m2m_client_ref.assigned_scopes IS
'Mirrored from Keycloak for display/audit; Keycloak remains the source of truth.';
CREATE INDEX idx_m2m_client_ref_participant ON m2m_client_ref (participant_id) WHERE is_deleted = false;
CREATE TRIGGER trg_m2m_client_ref_updated
BEFORE UPDATE ON m2m_client_ref
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- onboarding_request — intake; links to participant once approved
-- -----------------------------------------------------------------------------
CREATE TABLE onboarding_request (
request_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id uuid
REFERENCES participant (participant_id) ON DELETE SET NULL,
applicant_user_id varchar(255),
applicant_email varchar(255) NOT NULL,
applicant_name varchar(255) NOT NULL,
applicant_job_title varchar(255),
applicant_phone varchar(50),
legal_name varchar(255) NOT NULL,
kvk_number varchar(50),
lei varchar(20),
company_address text,
postal_code varchar(20),
city varchar(100),
country varchar(100),
status varchar(20) NOT NULL DEFAULT 'Submitted'
CONSTRAINT onboarding_request_status_check
CHECK (status IN ('Submitted','UnderReview','Approved','Rejected')),
submitted_at timestamptz NOT NULL DEFAULT now(),
reviewed_at timestamptz,
reviewed_by varchar(255),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
created_by varchar(100),
updated_by varchar(100),
is_deleted boolean NOT NULL DEFAULT false
);
COMMENT ON COLUMN onboarding_request.participant_id IS
'Set when the application is approved and a participant is created.';
COMMENT ON COLUMN onboarding_request.reviewed_by IS
'Keycloak user ID of the reviewer (opaque ID, no DB FK).';
CREATE INDEX idx_onboarding_request_status ON onboarding_request (status);
CREATE INDEX idx_onboarding_request_email ON onboarding_request (applicant_email);
CREATE TRIGGER trg_onboarding_request_updated
BEFORE UPDATE ON onboarding_request
FOR EACH ROW EXECUTE FUNCTION asr_set_updated_at();
-- -----------------------------------------------------------------------------
-- audit_record — append-only change log (no updated_at / is_deleted by design)
-- -----------------------------------------------------------------------------
CREATE TABLE audit_record (
audit_record_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type varchar(100) NOT NULL,
entity_key varchar(255) NOT NULL,
action varchar(20) NOT NULL
CONSTRAINT audit_record_action_check
CHECK (action IN ('Added','Modified','Deleted')),
performed_by varchar(100),
created_at timestamptz NOT NULL DEFAULT now(),
payload jsonb
);
CREATE INDEX idx_audit_record_entity ON audit_record (entity_type, entity_key);
CREATE INDEX idx_audit_record_created ON audit_record (created_at);
COMMIT;

In samenwerking met

Connected Trade NetworkConclusionData in LogisticsContargoInland Terminals GroupVan Berkel