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

Versioning

The EDK credential design store has explicit version-snapshot operations on top of the IDK CRUD interface. They are useful when you need a permanent record of what a design looked like at a particular moment ("this is the version approved for production on 2026-05-01"), or when you want to be able to recover an earlier state after a regrettable edit. They are not a change log: ordinary createCredentialDesign, updateCredentialDesign, and import/refresh operations do not produce versions automatically. You decide when a snapshot is meaningful and call createDesignVersion for it.

Read this page if you are building tooling that needs to operate on the version history (an admin UI with a "save as version" button, a CI step that snapshots after a promotion, an audit report). If you only consume designs at runtime, you can ignore this layer: getCredentialDesign and resolveCredentialDesign always return the current live state and never look at the version history.

What a Version Is

A version is an immutable JSON snapshot of one CredentialDesignRecord at the moment createDesignVersion is called. Snapshots are stored in a generic config_version table that the EDK uses for all versioned configuration types (design, DCQL, others); the per-design active-version pointer is stored separately in design_current_version. The metadata you get back is:

data class DesignVersion(
val designId: Uuid,
val tenantId: String,
val version: Int, // monotonically increasing per (tenantId, designId), starting at 1
val isLatest: Boolean, // true for the version the current-version pointer references
val createdAt: Instant,
val createdBy: String?, // session principal at snapshot time
)

CreateDesignVersionInput carries an optional description. It is recorded with the audit event for the snapshot operation; it is not stored on the version row itself, so do not rely on it as the audit trail.

What createDesignVersion Actually Does

It reads the current row from the credential repository and serialises it to the config_version table. That is the entire operation. From the implementation:

override suspend fun createDesignVersion(
tenantId: String, designId: Uuid, input: CreateDesignVersionInput,
): IdkResult<DesignVersion, IdkError> {
val design = credentialRepo.findById(tenantId, designId)
?: return Err(IdkError.NOT_FOUND_ERROR(...))
val contentJson = json.encodeToString(CredentialDesignRecord.serializer(), design)
val row = configVersionRepo.appendVersion(
tenantId = tenantId, domain = DOMAIN,
ownerKey = designId.toString(),
contentJson = contentJson, createdBy = execution.principalId,
)
currentVersionRepo.setCurrentVersion(tenantId, designId, row.version)
return Ok(DesignVersion(... isLatest = true ...))
}

The newly written version automatically becomes the current version. The live design row is untouched: snapshotting does not modify what getCredentialDesign returns.

Implication: if you edit a design (via updateCredentialDesign, importExternalDesign, or refreshCredentialDesign) after taking a snapshot, the snapshot still reflects the pre-edit state. If you want a new snapshot of the post-edit state, you must call createDesignVersion again.

How the Current-Version Pointer Is Used

The current-version pointer is a label. It marks one version as "the latest" for display in listDesignVersions. It is not consulted by getCredentialDesign, findCredentialDesignByBinding, resolveCredentialDesign, or any other read path. The live design content always comes from the live row in the credential repository; the pointer never reroutes reads.

This matters if you are designing a workflow around versions. "Roll back to version 3" cannot be done by repointing alone; see Rollback below.

Reading the History

listDesignVersions returns the version metadata for one design, with isLatest set on whichever row the pointer references:

val versions: List<DesignVersion> = service
.listDesignVersions(tenantId, designId)
.getOrElse { return }
for (v in versions) {
val marker = if (v.isLatest) " (current pointer)" else ""
println("v${v.version} by ${v.createdBy ?: "system"} at ${v.createdAt}$marker")
}

getDesignVersionContent returns the full snapshot for one version, deserialised into a CredentialDesignRecord:

val snapshot: CredentialDesignRecord = service
.getDesignVersionContent(tenantId, designId, version = 3)
.getOrElse { return }

The snapshot is the same record type as a live read, so anything that consumes a current design (rendering, the resolution engine, OID4VCI metadata generation) can also consume a historical snapshot.

What setLatestDesignVersion Does

It updates only the current-version pointer. The live design row is untouched, and (as above) ordinary reads do not consult the pointer. Use it to label a different version as "current" for the purposes of listDesignVersions display and any tooling you build that filters on isLatest. Do not use it expecting the live design to change.

service.setLatestDesignVersion(tenantId, designId, version = 4)
// `listDesignVersions` now flags v4 as isLatest = true.
// `getCredentialDesign` still returns whatever the live row says.

If your operator UI needs "click here to make this version live", that UI must do two things, in this order: read the content with getDesignVersionContent, then apply it with updateCredentialDesign (covered below).

Rollback

To actually restore an earlier state into the live design, read the snapshot and write it back through the regular update path:

suspend fun rollback(
service: VersionedCredentialDesignService,
tenantId: String,
designId: Uuid,
version: Int,
): IdkResult<CredentialDesignRecord, IdkError> {
val snapshot = service
.getDesignVersionContent(tenantId, designId, version)
.getOrElse { return Err(it) }

val updateInput = UpdateCredentialDesignInput(
alias = snapshot.alias,
bindings = snapshot.bindings,
credentialTemplateId = snapshot.credentialTemplateId,
issuerDesignId = snapshot.issuerDesignId,
displays = snapshot.displays,
claims = snapshot.claims,
renderVariantIds = snapshot.renderVariantIds,
hostingMode = snapshot.hostingMode,
)
return service.updateCredentialDesign(tenantId, designId, updateInput)
}

After this, you almost certainly want to call createDesignVersion(designId, ...) so the rollback action itself is recorded as a fresh version. That gives you a clean trail: "v3 was the live design. v4 was an edit. v5 is a rollback to v3's content." If you skip the snapshot, the rollback is invisible in the version history (only the live row's updatedAt changes).

Render variants and assets are referenced by id from renderVariantIds. The snapshot preserves those ids, but the variants themselves are separate records that are not part of the credential design's version history. If you have also deleted or changed a render variant since the snapshot was taken, a rollback restores the id list but the referenced variants may no longer exist or may have different content. Treat render variants as long-lived shared resources rather than per-version data.

When to Take a Snapshot

There is no general right answer; pattern by what your operations team needs to be able to prove or recover. Common triggers:

  • After importing a design from an external source (importExternalDesign, refreshCredentialDesign). The snapshot captures the imported state so a later upstream change can be compared.
  • Before a risky edit (a binding change, a claim removal, a render variant swap) where you want a guaranteed restore point.
  • At a compliance checkpoint ("approved by the compliance review on 2026-05-01"). The description field on CreateDesignVersionInput is the right place for that text.
  • After a promotion from staging to production (in CI, immediately after updateCredentialDesign lands the staged content).

There is no garbage collection of old versions. The history grows monotonically per design.

Audit Trail

DesignVersion.createdBy is the session principal at snapshot time; createdAt is the snapshot timestamp. The standard EDK audit pipeline also records the credential-design.versions.* command executions with the actor, tenant, correlation id, and trace id, so for a compliance investigation you can correlate the version row with the originating audit event. Edits between snapshots leave updatedAt on the live row and a normal audit event for the updateCredentialDesign command, but no version row of their own.