Credential Design in the EDK
The IDK credential design guide is the conceptual reference. It explains what a credential design is, what a binding does, how claim presentations work, how the resolution engine combines layers, and what each format mapper (OID4VCI, SD-JWT VCT, JSON Schema, JSON-LD context, W3C render method, OCA) contributes. None of that changes in the EDK. The Kotlin interface you call is the same CredentialDesignService; the records you get back are the same CredentialDesignRecord, IssuerDesignRecord, VerifierDesignRecord, RenderVariantRecord, ResolvedCredentialDesign. Examples written for the IDK service work against the EDK service without modification.
What the EDK gives you is a different set of bindings of that interface. Most consumers do not call the EDK-specific service directly; they just include the right modules and the IDK interface they already use is now backed by SQL, fronted by HTTP, optionally cached, optionally version-snapshotted.
What You Get By Default
Add the EDK lib-data-store-credential-design-impl plus one of the persistence modules (-persistence-postgresql or -persistence-mysql) to your service. Through Metro DI's replaces mechanism, the binding for CredentialDesignService swaps from the IDK's DefaultCredentialDesignService (blob-backed) to the EDK's DefaultVersionedCredentialDesignService (SQL-backed). Existing code that injects CredentialDesignService and calls getCredentialDesign, listCredentialDesigns, resolveCredentialDesign, importExternalDesign, etc. continues to work, now hitting your relational database.
You also get the VersionedCredentialDesignService interface which extends CredentialDesignService with four extra operations:
interface VersionedCredentialDesignService : CredentialDesignService {
suspend fun createDesignVersion(
tenantId: String, designId: Uuid, input: CreateDesignVersionInput,
): IdkResult<DesignVersion, IdkError>
suspend fun listDesignVersions(
tenantId: String, designId: Uuid,
): IdkResult<List<DesignVersion>, IdkError>
suspend fun getDesignVersionContent(
tenantId: String, designId: Uuid, version: Int,
): IdkResult<CredentialDesignRecord, IdkError>
suspend fun setLatestDesignVersion(
tenantId: String, designId: Uuid, version: Int,
): IdkResult<DesignVersion, IdkError>
}
Inject this interface (instead of CredentialDesignService) only when you actually need the version operations. See Versioning for what they do and what they do not do.
What You Need To Decide
Three deployment choices, and what they mean in code:
Persistence backend. Pick one of -persistence-postgresql or -persistence-mysql. Both implement the same repository contracts; they share SQLDelight-generated query code with dialect-specific tweaks for JSON column types. The choice is operational, not architectural. Multi-tenant database routing is handled by the standard EDK database routing layer; the design store does not have its own routing.
REST exposure. If anything outside the JVM (a UI, a CLI, a CI pipeline, another service) needs to manage designs, include lib-data-store-credential-design-rest-vdx on the server side. That module registers CredentialDesignHttpAdapter into the session scope, and the Universal HTTP Adapter mounts it under /api/v1/designs/.... The full endpoint reference is in REST API. If your service only consumes designs internally, you do not need this module.
Offline cache. If your service is a wallet or verifier UI that must keep rendering credentials when the network is down, wrap the remote CredentialDesignService with CachingCredentialDesignService from the -cache module. See Persistence. For server-side services that have direct database access, the cache is not relevant.
How a Developer Actually Uses the Service
The IDK guide has the full method reference. The patterns that show up most often in real code:
Read a design by binding. Most consumers do not have a design id; they have a credential type identifier (a CREDENTIAL_CONFIGURATION_ID, a VCT, an mDoc doctype) and want the design that matches. Use findCredentialDesignByBindingKey:
val designs = service.findCredentialDesignByBindingKey(
tenantId = tenantId,
bindingKey = DesignBindingKey.CREDENTIAL_CONFIGURATION_ID,
bindingValue = "IdentityCredential",
).getOrElse { emptyList() }
val record: CredentialDesignRecord? = designs.firstOrNull()
A single design can have multiple bindings (a VCT plus a CREDENTIAL_CONFIGURATION_ID plus an OCA SAID), so the lookup works regardless of which identifier the caller has.
Resolve a design before rendering or before issuance. The find returns the bare stored record; the resolve runs it through the layered resolution engine, pulling in OID4VCI metadata, SD-JWT VCT, JSON Schema hints, render method content, OCA overlays, and local overrides in priority order. The result is what you render:
val resolved = service.resolveCredentialDesign(
tenantId = tenantId,
input = ResolveCredentialDesignInput(
designId = record.id,
preferredLocales = listOf("en", "nl"),
renderTarget = RenderVariantKind.SIMPLE_CARD,
),
).getOrElse { return }
// Read display metadata for UI
val display = resolved.design.displays.firstOrNull { it.locale == "en" }
// Read claim policy (mandatory + selective-disclosure) for issuance
val mandatory: Set<DesignClaimPath> =
resolved.design.claims.filter { it.mandatory }.map { it.path }.toSet()
val sdPolicies: Map<DesignClaimPath, SdPolicy> =
resolved.design.claims.associate { it.path to it.sdPolicy }
The same resolved.design.claims list is what the OID4VCI issuer translates into its runtime selective-disclosure and mandatory-claim policy (see DeferredPipelineReExecutor.mapDesignToContext). Display fields and policy fields live on the same record, so a UI render and an issuance decision can both read from the same resolved design.
Build OID4VCI metadata from designs. A credential design with a CREDENTIAL_CONFIGURATION_ID binding is the canonical source for one entry in credential_configurations_supported. List designs, filter for ones with that binding, resolve each, and pass each through Oid4vciDesignMapper.toCredentialConfiguration(resolved, format) to get the wire-format object. This is exactly what DesignBackedOid4vciIssuerConfigProvider does in the EDK OID4VCI issuer. If you are building an OID4VCI issuer of your own, follow that pattern; if you are using the EDK issuer service, it does this for you and the only thing you need to manage is the designs themselves.
Import a design from an external source. OID4VCI issuer metadata, SD-JWT VCT metadata, an OCA bundle URL, a JSON Schema URL: any of these can be turned into a persisted credential design through importExternalDesign. The server fetches the source through the SSRF-protected DesignExternalFetcher, snapshots the response as a SourceSnapshotRecord, runs the appropriate format mapper, and writes the resulting design. The snapshot is preserved so refreshCredentialDesign can later compare with If-None-Match and re-import only when the source has changed.
The full method reference (CRUD per entity type, render variants, asset upload/download, snapshots, all argument shapes) is in the IDK Working with Designs and Resolution and Import guides.
What's in This Section
- REST API: every HTTP endpoint, with request/response shapes and tenancy/authorization rules
- Versioning: how the explicit-snapshot operations work, what "current version" actually points at, and how to do rollback
- Persistence: PostgreSQL and MySQL repositories, what tables they own, and how the offline cache wrapper behaves
- OCA Bundles: OCA as a credential design source