CLR export — design (v1)
Status: implemented & validated 2026-06-15 (slices 5a–5c, branch clr-export) — the
credential lane’s last v1-milestone item. This is the Comprehensive Learner Record
assembler — slice 5 of credential-export-v1.md, specified here once single-credential
OB 3.0 signing was proven (slices 1–4 done 2026-06-13). Built to this design with one
refinement recorded inline: the inner achievements keep their own org issuer
(decision 1), so the shared issuer/subject/email/alignment resolution moved to
apps/api/src/features/credential/export-shared.ts, consumed by both export services.
validate:full 58/58, credential integration 7/7 (OB regression + CLR), and the CLR
conformance lane (bun run test:clr:conformance) green against the official 1EdTech
clr_v2p0_clrcredential schema. It adds no schema: a CLR is assembled, signed, and served on the
exact issuer / key / KeySigner / JWKS / sign-event machinery the OB lane already
ships. The work is an assembler (wire.ts), an export endpoint, and a conformance
lane — nothing the operator must provision beyond what OB already needed.
Decisions this builds on: ADR-0013 (person-rooted identity bindings — the pairwise
subject), ADR-0014 (erasure is anonymization — signed documents are un-erasable, so
no PII is frozen in), ADR-0015 (per-org issuer Profiles, platform-custodial keys,
evidence by reference, sign-on-export), ADR-0016 (VC-JOSE/JWT proofs with Ed25519),
ADR-0017 (Supabase Vault custody behind a KeySigner seam).
Design groundwork: credential-export-v1.md (the OB lane this extends — every
decision below inherits its decisions 1–9 verbatim), lrs-clr-record-mapping.md
(the record-mapping doc whose two open decisions — CLR profile granularity and
redaction policy — this design resolves), case-competency-v1.md (durable
competency-node URIs the inner credentials align to).
Boundary contracts: @conform-ed/contracts clr/v2_0 (ClrCredential,
ClrSubject, VerifiableCredential, Association, re-exporting OB v3_0
Achievement/AchievementCredential/IdentityObject), open-badges/v3_0,
vc-data-model/v2_0.
What a CLR is here
Section titled “What a CLR is here”A ClrCredential is a Verifiable Credential whose subject (ClrSubject) carries a
required verifiableCredential[] — the bundle. v1 fills that bundle with the
learner’s granted-award achievements (the same OB 3.0 bodies slice 2 already
assembles), wraps them in one signed envelope, and serves the compact JWS. The CLR
is the learner’s portable transcript: one signature over their whole record, each
constituent achievement still naming its own issuer.
Decisions (this design)
Section titled “Decisions (this design)”These extend credential-export-v1.md decisions 1–9; the issuer, custody, proof,
pairwise-subject, sign-on-export, and evidence-by-reference rules are unchanged.
-
The CLR is the platform-transcript comprehensive record. One CLR per learner, signed by the platform-default issuer Profile (the null-org row) acting as transcript issuer, bundling every
grantedqualification_awardfor that person across all orgs. A CLR is a single VC with a singleissuer, so it cannot span signers — the platform transcript issuer vouches for the collection, while each bundled achievement keeps its ownissuerfield. A per-issuer subset CLR (only one org’s awards) is a later query-scoped variant, not v1. -
Inner credentials are embedded as AchievementCredential objects, signed once by the CLR envelope. The bundle entries are the OB 3.0 AchievementCredential bodies (reusing
assembleAchievementCredential), embedded as JSON; the outer CLR’s enveloping JWS is the single proof over the whole record. Inner credentials are not independently signed in v1 — the comprehensive bundle is the unit of verification. Independently-signed inner credentials embedded as W3CEnvelopedVerifiableCredentialdata:application/vc+jwt,…entries is the named upgrade (it needs a contract slot for the enveloped form and is an ADR-0016 proof-nesting decision); deferred. -
One coherent subject across the bundle. Every subject id — the
ClrSubjectidand each inner credential’scredentialSubject.id— is the same pairwise binding for (person, transcript issuer): reuse-or-mint theidentity_binding(provider = 'credential-subject',issuer = transcript_issuer_uri). The hashed-emailIdentityObjectsits once atClrSubject.identifier; inner credentials omit it (no repeated salt exposure). Because every id is the transcript-scoped pairwise URI, the CLR never leaks the learner’s per-org pairwise ids — cross-issuer correlation stays sealed (ADR-0013). -
Sign-on-export, deterministic id, nothing stored. The CLR
idisurn:uuid:{uuidv5("clr:" + personId + ":" + transcriptProfileId, CREDENTIAL_NS)}— stable per (learner, transcript issuer), so re-export is stable while the bundle content reflects the learner’s current granted awards.validFromandawardedDateare the export instant. Mirrors OB decision 6 exactly: a stable identifier slot honored by derivation, not a stored artifact. -
Zero new schema. The CLR reuses
issuer_profile,issuer_signing_key, andcredential_sign_event. The sign-event logs the CLR withaward_id = null(the bundle is not one award; thecredential_idurn +payload_sha256digest are the tamper-evidence) — the constituent awards are deliberately not fanned out into per-award audit rows, so accountability gains no person-linkage breadth (ADR-0014). The only touch to existing code is widening the KeySigner seam’sawardIdto nullable. No migration, so no DB reset and no operator handoff for this slice. -
Redaction by construction (resolves
lrs-clr-record-mapping.mddecision 3). The CLR exposes only the OB-projected achievement surface — claims,Achievement.alignmentto durable CASE nodes, and provenance references (ADR-0014/0015). No institution-private field, raw xAPI excerpt, or evidence binary enters the bundle (decision 2 there is already resolved: reference only, never embedded). A configurable redaction policy waits for a named private field that some consumer needs conditionally included. -
Associations deferred.
ClrSubject.association(the CLR AssociationType vocabulary —isPartOf,isChildOf,precedes, …) is emitted empty in v1: the first CLR is a flat bundle. Projecting qualification hierarchy or prerequisite chains as associations is an additive later slice — the same way OBAchievement.alignmentarrived as slice 4 rather than at the start. -
Conformance-first. A CLR conformance lane (
bun run test:clr:conformance) validates the signed CLR against the official 1EdTechclr_v2p0_clrcredentialJSON Schema, wired with the slice — not endgame QA — exactly as the OB lane validates againstob_v3p0_achievementcredential.
Assembly (apps/api/src/features/credential/wire.ts)
Section titled “Assembly (apps/api/src/features/credential/wire.ts)”The OB→core boundary already lives here; the CLR adds a second assembler beside
assembleAchievementCredential, reusing it for each bundle entry.
const W3C_VC_V2_CONTEXT = "https://www.w3.org/ns/credentials/v2"; // existingconst CLR_V20_CONTEXT = "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.0.json";const OPEN_BADGES_V30_CONTEXT = "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"; // existing
export type AssembleClrCredentialInput = { clrId: string; // urn:uuid (decision 4) issuerUri: string; // the transcript issuer Profile URI issuerName: string; subjectUri: string; // the (person, transcript-issuer) pairwise URI name: string; // e.g. "Comprehensive Learner Record" validFrom: Date; // export instant identityObject?: Record<string, unknown>; // hashed email, once, at ClrSubject credentials: Record<string, unknown>[]; // the inner AchievementCredential bodies};deterministicClrId(personId, transcriptProfileId)— theurn:uuid:uuidv5(decision 4), reusing the existinguuidV5+CREDENTIAL_ID_NAMESPACE.assembleClrCredential(input)shapes:@context:[W3C_VC_V2_CONTEXT, CLR_V20_CONTEXT, OPEN_BADGES_V30_CONTEXT](the orderClrCredentialContextSchemarequires).type:["VerifiableCredential", "ClrCredential"].id,name,validFrom+awardedDate(=validFrom),issuer:{ id: issuerUri, type: "Profile", name: issuerName }.credentialSubject(ClrSubject):{ id: subjectUri, type: "ClrSubject", verifiableCredential: credentials, ...(identityObject ? { identifier: [identityObject] } : {}) }. Noproof— the enveloping JWS is the proof (ADR-0016).
parseClrCredential(candidate)—ClrCredentialSchema.safeParse, the fail-closed totality guard (mirrorsparseAchievementCredential/parseCfPackage). It validates the whole bundle, so any inner-credential shape theclr/v2_0contract rejects is caught at build time, before signing.
Inner entries are assembled in embedded mode: assembleAchievementCredential
called with subjectUri = the transcript pairwise URI and no identityObject
(decision 3). Slice-4 alignment carries through unchanged — each inner achievement
keeps its Achievement.alignment to durable CASE node URIs.
Export pipeline (apps/api/src/features/credential/export-service.ts)
Section titled “Export pipeline (apps/api/src/features/credential/export-service.ts)”POST /workflow/credentials/clr — the authenticated learner mints their own
comprehensive record. Nothing is persisted but the one sign-event row.
- Authz (learner-self). The subject is
principal.personId; no per-award ownership check beyond “the learner’s own awards”. Resolve the platform-default issuer profile (null org) as transcript issuer; if none is provisioned →409(the platform issuer must exist, an operator step). - Gather. Load all
state = 'granted'qualification_awardrows for the person. Zero granted awards →409(ClrSubject.verifiableCredentialis required and non-empty; there is nothing to record). Order deterministically (bygranted_at, then award id) so re-export is byte-stable. - Subject identity. Reuse-or-mint the (person, transcript-issuer) pairwise
identity_binding; read/generate the salt inbinding_meta; build the hashed-emailIdentityObjectfrom a person email contact point (omit when the learner has no email — the pairwiseidalone is a valid subject). - Assemble inner credentials. For each award, load its
qualification_definition+competency_alignment(the slice-4loadAchievementAlignmentslateral join, reused) and callassembleAchievementCredentialin embedded mode. Inner credential ids stay the deterministic per-awardurn:uuid(decision 6 of OB) — a verifier can still match a bundled achievement to its standalone OB export by id. - Assemble the CLR.
assembleClrCredential(...)over the inner bodies + identity object. - Totality guard.
parseClrCredential(candidate)— fail closed. - Sign once.
KeySigner.signCredentialJws({ issuerProfileId: transcriptProfileId, credentialId: clrId, awardId: null, payload })→ one compact JWS, onecredential_sign_event(award_id = null). - Return the compact JWS (
application/vc+jwt-style). Nothing signed is stored.
CredentialExportError (the existing descriptor + httpStatus pattern) covers the
409s. The KeySigner seam change is one line — awardId: string | null — and the
insert binds null straight through.
JWKS + issuer surface
Section titled “JWKS + issuer surface”Unchanged. The CLR’s issuer.id resolves through the existing
GET /credentials/issuers/:profileId Profile document and its
.../jwks.json, served from issuer_signing_key.public_jwk with no vault read.
Verification is JWKS-first (ADR-0016): the platform-default issuer’s kid in the
JWS header resolves to the served public key.
Conformance wiring
Section titled “Conformance wiring”A CLR lane mirroring the OB one, beside run-ob-conform-ed.ts:
tools/scripts/run-clr-conform-ed.ts(delegates to the integration compose harness,EMERGENT_INTEGRATION_SEED_ENABLED=false) +bun run test:clr:conformance.tests/conformance/clr-credential.conformance.test.ts— boots the stack, provisions the platform issuer + key, grants ≥2 fixture awards (ideally from two orgs, to exercise the multi-issuer bundle),POSTs the export endpoint, fetches the JWKS + Profile, then asserts: VC-JOSE shape, thekidresolves and the envelope signature verifies, the bundle has one entry per granted award with a consistent transcript subject id, theIdentityObjectsits once at the subject — and, the authoritative assertion, the decoded CLR validates against the vendored official 1EdTechclr_v2p0_clrcredentialJSON Schema (draft 2019-09, viaajv) attests/conformance/fixtures/.- The vendored schema + provenance README (the OB fixture pattern); oxfmt
ignorePatternsalready excludes the fixtures dir. - Runbook: extend
docs/runbooks/ob-conformance-suite.md(or a siblingclr-conformance-suite.md) with thetest:clr:conformanceentry. - Add the CLR integration test to the
test:integration:composelist and the conformance lane to the relevant gate.
Sequencing (slice 5 of credential-export-v1.md)
Section titled “Sequencing (slice 5 of credential-export-v1.md)”5a. Assembler + unit tests. assembleClrCredential, deterministicClrId,
parseClrCredential in wire.ts; unit tests for bundle shape, deterministic id
stability, embedded-mode subject-id consistency (inner ids = transcript pairwise
uri), single top-level identity object, and the fail-closed guard.
5b. Export service + endpoint + integration. exportClrCredential, the
POST /workflow/credentials/clr route (path-alias + app.ts wiring, reusing the
OB deps), the one-line nullable-awardId KeySigner change, and a podman-compose
integration test: provision platform issuer + key → grant two awards (two orgs)
→ export → verify the envelope against the JWKS → decode and assert the two-entry
bundle, consistent subject, identity object, and per-award inner ids; plus a
second-learner isolation check (404/empty) and the zero-awards 409.
5c. Conformance lane. The runner + clr-credential.conformance.test.ts +
vendored official CLR schema + runbook + test:clr:conformance script; green
against the official 1EdTech schema is the lane’s validated bar (standards.md).
Deferred (recorded, not blocking)
Section titled “Deferred (recorded, not blocking)”- Independently-signed inner credentials via W3C
EnvelopedVerifiableCredential(data:application/vc+jwt,…) — each bundled achievement standalone-verifiable inside the CLR. Needs a contract slot for the enveloped form and is an ADR-0016 proof-nesting decision. - Per-issuer / scoped CLRs —
POST /workflow/credentials/issuers/:profileId/clrfor one org’s subset, andpartial: truepoint-in-time records. v1 is the single comprehensive transcript. - Associations —
ClrSubject.associationprojecting qualification hierarchy / prerequisites with the CLR AssociationType vocabulary (additive, like OB alignment was). credentialStatus/ revocation, Data Integrity proofs,did:webissuer, learner-held keys, first-party verify page, OpenBao transit custody — inherited deferrals fromcredential-export-v1.md; all apply identically to the CLR.GetClrCredentialsResponseREST surface — the CLR 2.0 protocol’s paged-fetch endpoint (compactJwsString[]); v1 is the single learner-pull export, the OB lane’s shape.