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

DCQL Store

The IDK ships a DcqlQueryConfigurationStore interface and a default key-value-backed implementation that is fine for tests and single-tenant developer setups but loses history on every write. The EDK replaces it with VersionedDcqlQueryConfigurationStore: same contract for ordinary reads and writes, plus an immutable per-query version trail backed by a relational database. Every write produces a new immutable row in a shared config_version table and advances a current-version pointer in a typed dcql_query header table. Reads through the IDK interface continue to return the body the current-version pointer references, so existing IDK-facing code keeps working without changes. The version-history operations are exposed as a small additional REST surface alongside the existing IDK DCQL admin endpoints.

Storage Model

The store keeps two related rows for every DCQL query:

The dcql_query table holds the mutable header keyed by (tenant_id, identifier). It stores the logical query identity (name, description, enabled), the current-version pointer (current_version), a soft-delete timestamp (deleted_at, omitted from normal reads), and created_at / updated_at audit columns. Updates to header metadata happen in place; only the DCQL body is versioned.

The shared config_version table holds the immutable history. Each row carries the full serialized DCQL body for one version of one query, plus created_at and created_by audit columns. The config_version table is also used by other versioned configuration types in the EDK (credential designs, for example), so the same versioning infrastructure is reused; the domain column on each row distinguishes them ("oid4vp-dcql" for DCQL).

data class DcqlQueryHeaderRow(
val tenantId: String,
val identifier: String,
val name: String,
val description: String? = null,
val enabled: Boolean = true,
val currentVersion: Int,
val createdAt: Long,
val updatedAt: Long,
)

data class DcqlQueryVersionRecord(
val identifier: String,
val version: Int,
val dcqlQuery: DcqlQuery,
val createdAt: Long,
val createdBy: String? = null,
)

data class DcqlQueryVersionSummary(
val version: Int,
val createdAt: Long,
val createdBy: String? = null,
)

Tenant isolation is enforced at the query layer: every repository method filters by tenantId, which is resolved from the session context, not from a wire-supplied argument. The store has no concept of a verifier; that's a layer above, handled by the verifier-DCQL binding system.

What Counts as a New Version

Every write through the IDK DcqlQueryConfigurationStore.put/putPersistent path appends a new immutable config_version row and advances the header's currentVersion. Translated to the HTTP surface:

  • POST /api/v1/oid4vp/dcql writes version 1 along with the header.
  • PUT /api/v1/oid4vp/dcql/{queryId} appends a new version.
  • PATCH /api/v1/oid4vp/dcql/{queryId} also appends a new version, even when only name, description, or enabled is touched. The version row carries the DCQL body; for a header-only PATCH the new row's body is identical to the previous one. This is a deliberate trade-off: the version history is monotonic and every admin action shows up as a row, at the cost of "no-op" body rows.
  • POST /api/v1/oid4vp/dcql/{queryId}/versions/{version}/restore appends a new version whose body equals the chosen historical version and advances the pointer. History rows are never mutated.

Deletes are soft: the header's deleted_at is set and the row stops appearing in normal reads. The version history is preserved internally even though the version-history endpoints return 404 for soft-deleted queries.

REST API

The full HTTP surface for managing DCQL queries and walking version history is documented on its own page, with request and response shapes per endpoint, error mapping, tenancy rules, and policy considerations: see DCQL REST API. In brief, the basic CRUD endpoints come from the IDK DcqlQueryAdminHttpAdapter (lib-oid4vp-dcql-store-rest) and the version-history endpoints come from the EDK DcqlQueryVersionHttpAdapter (lib-oid4vp-dcql-store-versioned-rest); both mount under /api/v1/oid4vp/dcql and share the oid4vp-dcql OpenAPI tag.

Choosing a Backend

Two persistence backends are shipped. PostgreSQL is the recommended default for new deployments; MySQL is provided for organisations that have standardised on MySQL.

ModuleBackend
lib-oid4vp-dcql-store-versioned-persistence-postgresqlPostgreSQL via SQLDelight
lib-oid4vp-dcql-store-versioned-persistence-mysqlMySQL via SQLDelight

Both backends share the same SQLDelight-generated repository surface (DcqlQueryHeaderRepository), so the rest of the EDK does not care which one is wired in. Include exactly one of the persistence modules; the version-history rows go into the shared config_version table that the EDK provides as part of its versioned-config infrastructure, so no additional setup is needed.

The repositories run inside the same multi-tenant database routing layer the rest of the EDK uses, so per-tenant database selection and shared-schema-with-tenant-column deployments both work without DCQL-specific configuration.

Resolution at Authorization-Request Creation Time

When the universal OID4VP backend (POST /oid4vp/backend/auth/requests) creates an authorization request for a given query_id, it does not embed the DCQL body inline. It resolves the body through the registered DcqlQueryResolver, which by default in the EDK is the version-aware resolver (EdkDcqlQueryResolver) and, when the VDX binding module is on the classpath, the verifier-aware resolver (VdxDcqlQueryResolver). The resolver picks the body from the versioned store, snapshots both the queryId and the version into the authorization session, and uses that pair to render the DCQL body in the authorization request the wallet eventually fetches. The version is part of the snapshot, so a DCQL update committed after the session was created does not affect the wallet's view of the request.

Audit

Every write through the versioned store and every version-history endpoint goes through the EDK PolicyCommandExtension, so authorization policies can scope DCQL admin and history operations independently. The standard EDK audit pipeline records every command execution; the createdBy column on the version row carries the actor for the data-side audit. Together, the audit event timestamp and the version row's audit columns let an investigator answer both "who did this" and "what did the DCQL body look like at any past moment".