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

Resolution and Import

The design resolution engine is the core of the credential design system. Rather than relying on a single source for display metadata, it combines information from multiple providers in a defined priority order, producing a complete design that reflects the best available metadata for any credential.

How Resolution Works

Design Resolution Flow

When you resolve a design, the engine follows these steps:

  1. Find the base design by matching the input's binding keys against stored designs
  2. Run the provider pipeline: each provider contributes displays, claims, and render variants from its metadata source
  3. Merge the layers: displays are merged by locale, claims by path, render variants collected. Higher-priority providers override lower-priority ones
  4. Apply field locks: authoritative providers can lock fields, preventing lower-priority sources from overwriting them
  5. Return the result as a ResolvedCredentialDesign with full tracking of which layers contributed what

The result includes not just the merged design, but also metadata about the resolution process: which providers contributed, which fields are locked, and the ETag for cache validation.

Resolving a Design

// Resolve by binding
val result = designService.resolveCredentialDesign(
tenantId = tenantId,
input = ResolveCredentialDesignInput(
bindingKey = DesignBindingKey.VCT,
bindingValue = "https://issuer.example.com/identity",
preferredLocales = listOf("en", "nl"),
renderTarget = RenderVariantKind.SIMPLE_CARD
)
)
val resolved = result.value // IdkResult<ResolvedCredentialDesign, IdkError>

// Access the merged design
val display = resolved.design.displays.first() // Best locale match
println("Credential: ${display.name}")
println("Description: ${display.description}")

// Access claim labels for UI rendering
resolved.design.claims.sortedBy { it.order }.forEach { claim ->
val label = claim.labels.firstOrNull { it.locale == "en" }
println("${label?.label}: ${claim.valueKind} (${claim.widgetHint})")
}

// Access the matched render variant
resolved.renderVariants.forEach { variant ->
println("Render: ${variant.kind} - ${variant.alias}")
}

// See which providers contributed
resolved.appliedLayers.forEach { layer ->
println("Layer: ${layer.sourceType} (priority ${layer.priority})")
}

You can also resolve by design ID directly if you already know which design to use, or resolve issuer and verifier designs the same way:

// Resolve by design ID
val result = designService.resolveCredentialDesign(
tenantId = tenantId,
input = ResolveCredentialDesignInput(designId = knownDesignId)
)
val resolved = result.value // IdkResult<ResolvedCredentialDesign, IdkError>

// Resolve issuer design
val issuerResolved = designService.resolveIssuerDesign(
tenantId = tenantId,
input = ResolveIssuerDesignInput(
bindingKey = DesignBindingKey.ISSUER_ID,
bindingValue = "https://issuer.example.com"
)
)

// Resolve verifier design
val verifierResolved = designService.resolveVerifierDesign(
tenantId = tenantId,
input = ResolveVerifierDesignInput(designId = verifierDesignId)
)

Design Layer Providers

The resolution engine uses an ordered pipeline of providers. Each provider is specialized for a particular metadata source and contributes the information it can extract.

Provider Priority (Credentials)

Providers run in this order, from lowest to highest priority. Later providers override earlier ones on conflict:

PriorityProviderWhat It Contributes
1Schema InferenceClaims derived from JSON schemas (names, types, cardinality)
2JSON-LD ContextDisplay hints from linked JSON-LD contexts
3Credential TemplateClaims and displays from IDK credential templates
4OID4VCI MetadataFull display info from OID4VCI credential_configurations_supported
5SD-JWT VCTType metadata from SD-JWT Verifiable Credential Type definitions
6W3C Render MethodRender variants from W3C VC Render Method references
7Local OverrideUser-defined designs stored in the IDK (highest priority)

Provider Priority (Issuers)

PriorityProviderWhat It Contributes
1OID4VCI Issuer MetadataDisplay info from .well-known/openid-credential-issuer
2Party StoreEntity branding from the IDK party management system
3Local OverrideUser-defined issuer designs (highest priority)

The local override provider always wins, which means you can always customize how a credential or issuer is displayed in your application regardless of what the external metadata says.

Field Locking

When a provider is marked as authoritative, the fields it contributes are locked; lower-priority providers can add new fields but cannot overwrite locked ones. This is useful when an issuer's own metadata (via OID4VCI or SD-JWT) should be considered the definitive source for certain display properties, while still allowing local customizations for fields the issuer didn't specify.

Importing External Designs

Instead of manually creating designs, you can import them from external metadata sources. The design service fetches the metadata, converts it using format mappers, and stores the result locally.

From OID4VCI Issuer Metadata

Import credential and issuer designs from an OID4VCI issuer's .well-known endpoint:

// Import credential designs from OID4VCI metadata
val imported = designService.importExternalDesign(
tenantId = tenantId,
input = ImportExternalDesignInput(
sourceUrl = "https://issuer.example.com/.well-known/openid-credential-issuer",
sourceType = DesignSourceType.OID4VCI_CREDENTIAL_CONFIGURATION,
entityType = DesignEntityType.CREDENTIAL,
bindings = listOf(
DesignBinding(
key = DesignBindingKey.CREDENTIAL_CONFIGURATION_ID,
value = "IdentityCredential"
)
)
)
)

// Import the issuer's branding
val issuerImported = designService.importIssuerDesign(
tenantId = tenantId,
input = ImportIssuerDesignInput(
sourceUrl = "https://issuer.example.com/.well-known/openid-credential-issuer",
sourceType = DesignSourceType.OID4VCI_ISSUER_METADATA,
bindings = listOf(
DesignBinding(
key = DesignBindingKey.ISSUER_ID,
value = "https://issuer.example.com"
)
)
)
)

When importing from OID4VCI metadata, the Oid4vciDesignMapper extracts credential names, descriptions, logos, claim definitions, and locale-specific displays from the credential_configurations_supported structure. This means a wallet can automatically pick up display metadata from any OID4VCI-compliant issuer without manual design creation.

From SD-JWT VCT Metadata

For SD-JWT credentials, type metadata can be fetched from the VCT URL:

val imported = designService.importExternalDesign(
tenantId = tenantId,
input = ImportExternalDesignInput(
sourceUrl = "https://issuer.example.com/.well-known/vct/identity",
sourceType = DesignSourceType.SD_JWT_VCT_METADATA,
entityType = DesignEntityType.CREDENTIAL,
bindings = listOf(
DesignBinding(
key = DesignBindingKey.VCT,
value = "https://issuer.example.com/identity"
)
)
)
)

Refreshing Imported Designs

Imported designs can be refreshed to pick up changes from the external source:

val refreshed = designService.refreshCredentialDesign(
tenantId = tenantId,
designId = importedDesign.id
)
// Uses ETag for conditional fetches to minimize network traffic

Source Snapshots

When a design is imported or refreshed, the raw external metadata is stored as a source snapshot. This serves as a cache and audit trail, allowing the IDK to re-derive the design without another network fetch and to compare against future versions.

val snapshot = designService.getSourceSnapshot(tenantId, snapshotId)
// snapshot.sourceUrl - Where it was fetched from
// snapshot.sourceType - OID4VCI, SD-JWT, etc.
// snapshot.content - Raw fetched content
// snapshot.etag - For conditional refreshes
// snapshot.fetchedAt - Timestamp

Configuration

The design module's behavior is controlled through CredentialDesignModuleConfig, which has three sections:

Resolution Policy

Controls how the resolution engine aggregates metadata from multiple sources:

credential-design:
policy:
schema-hints-enabled: true # Derive claims from JSON schemas
json-ld-context-hints-enabled: true # Extract hints from JSON-LD contexts
markdown-rendering-enabled: false # Allow markdown in claim values
issuer-metadata-preference: OID4VCI_FIRST # OID4VCI_FIRST | SD_JWT_FIRST | MERGE | LOCAL_ONLY
credential-metadata-preference: MERGE # How to combine format-specific metadata
prefer-integrity-protected-sources: true # Prefer sources with integrity protection
allow-remote-template-fetch: false # Fetch SVG/PDF templates from remote URLs
allow-remote-asset-fetch: false # Fetch logos/images from remote URLs

The issuerMetadataPreference setting is important when the same issuer has metadata available in multiple formats. OID4VCI_FIRST prioritizes OID4VCI issuer metadata, SD_JWT_FIRST prioritizes SD-JWT issuer metadata, MERGE combines both, and LOCAL_ONLY ignores external sources entirely.

Refresh Settings

Controls automatic refresh behavior for imported designs:

credential-design:
refresh:
enabled: false # Enable automatic refresh
default-ttl-seconds: 86400 # Re-fetch after 24 hours
rehost-remote-assets: true # Download and store remote assets locally
max-asset-size-bytes: 5242880 # 5 MB limit per asset

When rehost-remote-assets is enabled, logos and images referenced by imported designs are downloaded and stored in the IDK's blob store. This avoids runtime dependencies on external servers and ensures assets remain available even if the source goes offline.

Validation Limits

Guards against malformed or excessively large designs:

credential-design:
validation:
max-bindings-per-design: 16
max-displays-per-design: 64
max-claims-per-design: 256
max-render-variants-per-design: 32
max-entry-codes-per-claim: 1024
fail-on-unknown-source-type: true
validate-on-read: false # Re-validate designs when reading from store

Security

External metadata fetching includes built-in SSRF (Server-Side Request Forgery) protection. The DesignExternalFetcher blocks requests to private IP ranges and local network addresses, preventing imported design URLs from being used to probe internal infrastructure. This protection is always enabled and cannot be disabled.

Fetched content is also size-limited (configurable, default 10 MB) to prevent denial-of-service through oversized responses.

Data Types

ResolvedCredentialDesign

data class ResolvedCredentialDesign(
val design: CredentialDesignRecord, // Merged credential design
val issuerDesign: IssuerDesignRecord?, // Associated issuer branding
val verifierDesign: VerifierDesignRecord?, // Associated verifier display
val renderVariants: List<RenderVariantRecord>, // Matched render variants
val derivedRenderHints: DerivedRenderHintsRecord?, // Auto-derived field hints
val appliedLayers: List<AppliedDesignLayer>, // Which providers contributed
val lockedFields: Set<String>, // Fields locked by authoritative sources
val resolvedAt: Instant, // Resolution timestamp
val etag: String? // Cache validation tag
)

AppliedDesignLayer

data class AppliedDesignLayer(
val sourceType: DesignSourceType, // OID4VCI, SD_JWT, LOCAL_OVERRIDE, etc.
val sourceUrl: String?, // Where the metadata came from
val priority: Int, // Provider priority (higher wins)
val authoritative: Boolean // Whether this layer can lock fields
)