Master Data Registry
wallet.credentials alone stores a credential as an opaque JSON blob keyed only to issuer_id —
nothing for an institution’s actual regulatory obligations (reporting, identifier verification,
framework-aligned qualifications) to attach to. This is the master data model that fills that gap:
jurisdictions, qualification frameworks, catalog entries, code sets, regulatory schemes, and
learner records.
Registration → issuance flow
Three layers, one governing rule
Build once, configure as needed: what’s true in every jurisdiction lives in schema; what’s true only under one country’s regulation lives in data.
| Layer | Contains | Changes when |
|---|---|---|
| Core domain | learner, catalog_entry, enrolment, outcome, issuer | The product’s capabilities change |
| Configuration | jurisdiction, qualification_framework, code_set, regulatory_scheme | A new market is onboarded — data only |
| Adapter | scheme_field_map, export jobs (NAT files, SDR, ILR…) | A regulator changes its export format |
Onboarding a new country is a configuration exercise (seed data + at most one new export job), not a migration against core tables.
Service & schema ownership
| Schema | Service | Holds | Sensitivity | REST | gRPC |
|---|---|---|---|---|---|
catalog | Program Catalog | Jurisdictions, frameworks, catalog entries + versions, code sets, schemes, issuer scheme registrations | Public/internal reference | 8085 (actuator only) | 50054 |
learner | Learner Records | Learner identity, national identifiers, enrolments, outcomes | Restricted PII | 8086 (actuator only) | 50055 |
authority | Issuer Registry | Issuers, DIDs, keys | Confidential | 8083 | 50053 |
wallet | Credential Issuance | Credentials, extended with soft FKs below | Confidential | 8081 | 50051 |
Learner Records is its own service — not folded into Program Catalog — because a national identifier plus date of birth is regulated PII (Privacy Act 1988, GDPR, UK-DPA, PIPEDA). One service means one encryption-at-rest policy and one retention clock.
Core entities
| Table | Key columns | Note |
|---|---|---|
catalog.jurisdiction | jurisdiction_code (PK), name, regulator_name, status | ISO 3166-1/2 codes, e.g. AU, AU-QLD, NZ |
catalog.qualification_framework / framework_level | rank, label | AQF, NZQCF, EQF, RQF, SCQF, NSQF, DQR, CNCP/RNCP, QFEmirates, ISCED 2011 — only frameworks with a numbered level structure are seeded |
catalog.catalog_entry / catalog_entry_version | entry_type: QUALIFICATION · UNIT · SKILL_SET · MICRO_CREDENTIAL · IDENTITY_DOCUMENT · LICENSE | Versioned — superseding inserts a new row, never mutates the old one |
catalog.external_reference | owner_table, owner_id, register_code, external_id | Polymorphic pointer to any external register (TGA, NZQA, …) |
learner.learner / learner_identifier | legal_name, dob (encrypted); identifier_type: USI · NSN · … | |
learner.enrolment | delivery_mode, funding_source (both code_set FKs) | Learner + catalog entry version + issuer, over a period |
learner.outcome | result_code, is_rpl, assessor_ref | The fact a credential ultimately certifies |
wallet.credentials (extended) | learner_id, catalog_entry_version_id, outcome_id — all nullable soft FKs | Null for credentials issued outside a tracked enrolment (most BYOK issuers) |
Catalog entry versioning
At most one version per catalog_entry_id has effective_to IS NULL (the current version),
enforced with:
CREATE UNIQUE INDEX idx_catalog_entry_version_current
ON catalog.catalog_entry_version (catalog_entry_id)
WHERE effective_to IS NULL;A credential issued under version N remains fully resolvable forever — rows are never mutated or deleted.
Configuration entities
| Table | Purpose |
|---|---|
catalog.code_set / code_value | The one generic lookup mechanism — every jurisdiction-specific enum (delivery mode, funding source, outcome result, identifier type) is rows here, never a CHECK constraint |
catalog.regulatory_scheme / scheme_field_map | A named reporting/identity-verification protocol, plus a data-driven map from core columns to that scheme’s export fields |
catalog.issuer_scheme_registration | Links an issuer to a jurisdiction’s regulator: registration number + scope (JSONB array of catalog_entry_id) + status |
scheme_field_map.source_expr resolution strategies
source_kind | Meaning | Example |
|---|---|---|
FIELD_PATH | Dot-path into the core entity graph | outcome.result_code |
CONSTANT | Literal value | "AU" |
CODE_LOOKUP | Dot-path, resolved through code_set | enrolment.delivery_mode → NAT00090 code |
CONCAT | |-separated list joined | learner.family_name|learner.given_name |
One FieldMapResolver implementation per source_kind; adding a fifth strategy is strictly
additive.
PII encryption
learner_identifier.identifier_value_ciphertext uses AES-256-GCM at the application layer, with
its own master key (LEARNER_REGISTRY_KMS_MASTER_KEY) — never shared with signing-key or SSO
secrets, so a compromise of one doesn’t expose the others. The cipher primitive itself
(AesGcmFieldEncryptor) lives in backends/shared, reused by every service that needs
field-level encryption.
Referential integrity
All cross-schema references (catalog ↔ learner ↔ authority ↔ wallet) are soft FKs,
validated via gRPC at write time — see Database Schema for the
soft-FK pattern.
Identity documents and licences
Two catalog_entry types are not learning outcomes:
IDENTITY_DOCUMENT— establishes who someone is: passports, national ID cards, residence and work permits, visas, civil-status records.LICENSE— authorises an activity: driving, flying, a profession, working with children.
These exist because Custodians (Custodian Onboarding) collect
and attest exactly these documents. Seeded per-jurisdiction (AU, US, GB, IN, CA, SG,
AE, NZ, EU, DE, FR, plus a GLOBAL default) — the custodian picker searches every
jurisdiction, not only the custodian’s own, ranked with a small preference for the custodian’s own
jurisdiction and GLOBAL first. Only records that never lapse (civil-status records, lifetime
numbers) are seeded never_expires; everything else leaves validity unset since the printed
expiry governs.