Skip to main content
Version: v0.25.0 (Latest)

Credential Design Integration

The EDK glues OCA into the credential design system through three components. The OcaDesignMapper converts a parsed OCA bundle into the canonical credential design model used by the IDK (claim presentations, displays, derived render hints). The OcaBundleDesignProvider plugs OCA into the IDK's design resolution pipeline as a layer provider, so calls to resolveCredentialDesign can fold OCA-derived metadata into the result alongside OID4VCI metadata, SD-JWT VCT, and local overrides. The OcaNativePersistenceService stores the original OCA records alongside the canonical design so the bundle remains round-trippable even after the design has been mapped, edited, and republished.

This page explains how the three components fit together and what each one is responsible for. The underlying credential design model (claim paths, value kinds, widget hints, render variants) is documented in the IDK credential design guide and is not reshaped by the OCA integration; OCA is one of several sources that feed it.

The Mapper

OcaDesignMapper.toCanonical(bundle, bindings) is the conversion entry point. It accepts a ParsedOcaBundle and a list of DesignBindings and returns an OcaCanonicalResult containing:

  • A CredentialDesignRecord with the bindings, locale-aware displays, and a ClaimPresentation per attribute, ordered as they appear in the capture base
  • A DerivedRenderHintsRecord with per-field FieldRenderHint entries carrying the OCA-derived value kind, widget hint, character encoding, format pattern, standard reference, parsed cardinality, and sensitivity flag
  • The same displays and claims as flat lists, in case the caller wants them directly

The mapper delegates the overlay walk to OcaOverlayProcessor (described in Bundle Service), then maps the processed result onto the design model types. The translation is mechanical:

OCA ConceptCanonical Mapping
Attribute and its typeClaimPresentation with valueKind from OcaTypeMapping.valueKind
Attribute with no entry codeswidgetHint from OcaTypeMapping.defaultWidget
Attribute with entry codeswidgetHint = ClaimWidgetHint.PICKLIST (overrides the default)
label overlay per localeClaimLabel(locale, label) added to the claim's labels
information overlay per localeClaimLabel.description for the matching locale
entry overlay per localeClaimLabel.entryValues for the matching locale
conformance mandatory flagClaimPresentation.mandatory = true
cardinality stringClaimCardinality(min, max) via parseCardinality
sensitive overlayFieldRenderHint.sensitive = true
unit overlayClaimPresentation.unit
meta overlay per localeLocalizedCredentialDisplay(locale, name, description)
format overlayFieldRenderHint.formatHint
character_encoding overlayFieldRenderHint.characterEncoding
standard overlayFieldRenderHint.standard

The bindings parameter lets the caller decide how the resulting design should be matched to credentials at resolution time. When no bindings are provided the mapper falls back to a single OCA-SAID binding (DesignBinding(ocaSaid = bundleCaptureBaseSaid)), which makes the design match credentials referenced by their capture base SAID. In practice, callers usually pass an explicit binding such as a CREDENTIAL_CONFIGURATION_ID or VCT so the OCA-derived design slots into the same lookup keys as designs from other sources.

The mapper does not touch render variants; those are the responsibility of separate ingestion paths (SVG templates, W3C render method references, simple cards). An OCA bundle that wants to carry a render template should attach the template through a render variant in the credential design system, not through OCA itself.

The Layer Provider

OcaBundleDesignProvider implements the IDK's DesignLayerProvider interface and registers itself with the design resolution engine. At resolve time, the provider looks for an OCA bundle in ResolveCredentialDesignInput.externalMetadata.ocaBundle, parses it, runs the SAID structural check, and runs the mapper to produce a CredentialDesignLayerResult. The layer result carries the displays, claims, and derived render hints contributed by OCA, plus a set of providedFields markers the engine uses to track which fields the OCA layer touched.

The provider's sourceType is DesignSourceType.OCA_BUNDLE and its authoritative flag is false by default, which means lower-priority providers can still contribute fields the OCA bundle did not cover, and higher-priority providers (notably LOCAL_OVERRIDE) win on conflict. If your deployment treats OCA as the canonical source for a particular credential type, set authoritative = true on a custom subclass and bind that instead; the engine will then field-lock everything the OCA layer contributes.

val resolved = designService.resolveCredentialDesign(
tenantId = tenantId,
input = ResolveCredentialDesignInput(
bindingKey = DesignBindingKey.VCT,
bindingValue = "https://issuer.example.com/identity",
preferredLocales = listOf("en", "nl"),
externalMetadata = ExternalDesignMetadata(
ocaBundle = bundleJson,
),
),
)

When the provider returns a non-null layer, the resolution engine merges its contributions in priority order with everything else (schema inference, JSON-LD context, OID4VCI metadata, SD-JWT VCT, W3C render method, local override). The full priority list is in the IDK resolution and import guide.

Native Persistence

When an OCA bundle is imported through the design service, the canonical credential design produced by the mapper is what the IDK stores. The original OCA records are not kept by the design store itself. The EDK provides a parallel native persistence path so the original bundle and overlays can be retrieved unchanged later, for republication, for export, or just for audit.

OcaNativePersistenceService.persistBundle(tenantId, bundle, designId, sourceSnapshotId) writes an OcaBundleRecord for the bundle and an OcaOverlayRecord for each overlay, linked back to the corresponding CredentialDesignRecord via designId and to the original fetched payload via sourceSnapshotId. The records carry every overlay type and language present in the bundle, plus the original payload JSON, so the bundle can be reassembled byte-for-byte.

The two repository interfaces:

interface OcaBundleRepository {
suspend fun findById(tenantId: String, id: Uuid): OcaBundleRecord?
suspend fun findByBundleSaid(tenantId: String, bundleSaid: String): OcaBundleRecord?
suspend fun findByDesignId(tenantId: String, designId: Uuid): List<OcaBundleRecord>
suspend fun create(record: OcaBundleRecord): OcaBundleRecord
suspend fun delete(tenantId: String, id: Uuid): Boolean
}

interface OcaOverlayRepository {
suspend fun findById(tenantId: String, id: Uuid): OcaOverlayRecord?
suspend fun findByBundleId(tenantId: String, bundleId: Uuid): List<OcaOverlayRecord>
suspend fun create(record: OcaOverlayRecord): OcaOverlayRecord
suspend fun delete(tenantId: String, id: Uuid): Boolean
}

The dialect-specific implementations follow the same Postgres*Repository / Mysql*Repository pattern as the rest of the credential design persistence layer (see Persistence and Caching). The records persist as native JSON for the overlay payloads with a small number of indexed top-level columns (bundle_said, capture_base_said, design_id, tenant_id) for efficient lookups.

Two additional record types exist for richer OCA workflows:

OcaOverlayDefinitionRecord describes a custom overlay schema. OCA supports user-defined overlay types beyond the core spec; this record stores the definition (namespace, name, version, unique-key fields, definition JSON) so an ecosystem can publish and reference its own overlays.

OcaAuthoringArtifactRecord describes the source files used to author a bundle (raw bundle JSON, capture base JSON, individual overlay JSON, OCAfile or overlay-file declarations). The kind enum (BUNDLE_JSON, CAPTURE_BASE_JSON, OVERLAY_JSON, OCAFILE, OVERLAYFILE) drives interpretation. The actual content is stored in the blob store under contentBlobPath; the record carries the metadata and links back to the design and source snapshot.

Both additional record types have their own repositories with the same dialect-specific implementations.

End-to-End Import Flow

The typical OCA import looks like this:

  1. The client calls importCredentialDesign (or the OCA-specific import variant) with the source URL or raw bundle.
  2. The design service fetches the bundle through the DesignExternalFetcher, which enforces SSRF protection and size limits.
  3. The fetched content is stored as a SourceSnapshotRecord, producing sourceSnapshotId.
  4. The bundle is parsed and the SAID is structurally verified.
  5. The mapper produces a canonical CredentialDesignRecord and DerivedRenderHintsRecord.
  6. The design service persists those records and creates a DesignVersion (see Versioning).
  7. The OcaNativePersistenceService writes the native OcaBundleRecord and OcaOverlayRecords, linked to the new designId and sourceSnapshotId.

After import, two consumers can read the design:

  • The IDK design service returns the canonical CredentialDesignRecord from getCredentialDesign (or any resolution call).
  • The OCA repositories return the native OcaBundleRecord and overlays from findByDesignId, which can be reassembled into the original bundle JSON for export or republication.

If the bundle is later refreshed because the external source changed, a new SourceSnapshotRecord is stored, the mapper runs again, a new DesignVersion is committed, and a new set of native OCA records is written. The historical OCA records remain queryable through the version-aware repository methods.

Driving Attribute Provisioning from a Bundle

The EDK OID4VCI attribute-contribution endpoint accepts an OCA bundle id directly in its compact request body. Inside each contribution group, a semanticAttributeSets entry carries a bundleId (and an optional version) plus a values map of term -> JSON value. The decoder resolves the bundle through the SemanticAttributeSetService, indexes the resulting SemanticAttributeDefinitions by attribute term, and for each term derives the wire-side fields the integrator would otherwise have to spell out:

  • path (the JSON Pointer the attribute lands at in the session bag)
  • valueKind (how the JSON value is wrapped as an AttributeValue)
  • dataClassification (e.g. PII, sensitive PII)
  • legalBasis (GDPR Art. 6 lawful basis, jurisdiction-specific values supported)
  • retentionPolicy (ephemeral, session, or retained with retentionDays and friends)
  • sensitive (drives masking and PII-handling pathways)
  • sdPolicy (selective-disclosure policy at credential-assembly time)

The same semanticAttributeSets[i].bundleId field is accepted by POST /api/v1/oid4vci/offers under preSeededGroups[i].semanticAttributeSets[i].bundleId, so an integrator can seed attributes at offer creation time using exactly the same notation.

A typical contribution becomes a small JSON payload that names the bundle and lists term-value pairs:

{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"values": {
"given_name": "Alice",
"family_name": "Smith",
"birth_date": "1990-04-01"
}
}
]
}
]
}

The server-side resolver call goes through SemanticAttributeSetService.getAttributeSetByName(bundleId, version). When version is omitted, the service returns the tenant's pinned active bundle version (falling back to the highest available version), so tenants pinned to an earlier bundle get deterministic resolution rather than silent drift. Unknown terms short-circuit the entire request with a single 400 response listing every unresolved item across all sets; the session is never partially updated.

Per-attribute overrides (retention, priority, assurance, verified, sourceDetail, sdPolicy) remain available through the semanticAttributeSets[i].attributes[] form, with the precedence per-item > set-level > group-level > semantic-derived > default. The wire shape and the precedence rules are documented in the OID4VCI REST API.

When to Pass OCA Inline vs. Import It

Two distinct flows use OCA bundles, and they call into different parts of the integration:

Inline resolution. A wallet has a credential and an OCA bundle that came with it (for example, attached to an OID4VCI offer or fetched on the side from a published location). The wallet calls resolveCredentialDesign with the bundle in externalMetadata.ocaBundle. The layer provider parses, maps, and contributes claims and displays for this single call, but nothing is persisted. The OCA bundle is treated as transient data.

Registry import. An issuer admin uploads or references an OCA bundle that should become a long-lived credential design in the registry. The bundle goes through the full import flow above: it is fetched, snapshotted, parsed, verified, mapped, persisted as a design, persisted natively as OCA records, and versioned. Subsequent resolution calls find the canonical design through its bindings and do not need to pass the OCA bundle again.

The inline path is what mobile wallets and one-off rendering use. The registry path is what issuer onboarding and verifier-side credential type catalogs use.