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

Verifier DCQL Bindings

A tenant typically defines its DCQL queries once at the tenant level. The same age_verification query lives in the versioned DCQL store and gets edited over time. But a tenant also typically hosts multiple verifiers (a checkout flow, a customer-portal flow, a third-party-relying-party integration), each of which may want to use the shared query at a specific version. The VDX VerifierDcqlBinding is the thin layer that captures that pin: which verifier uses which shared DCQL query, at which version, with which local alias and enabled flag.

This is a VDX-level concern, not a base EDK one. The IDK and the bare EDK ship a tenant-level versioned DCQL resolver; this module adds the per-verifier binding store, the verifier-aware resolver that replaces the tenant-level one, the REST surface for managing bindings, and the scheduled-activation projection over the EDK scheduler.

The Binding Model

data class VerifierDcqlBinding(
val id: String,
val tenantId: String,
val verifierId: String,
val dcqlQueryId: String,
val pinnedVersion: Int,
val enabled: Boolean = true,
val alias: String? = null,
val createdAt: Instant,
val updatedAt: Instant,
)

A binding is identified by (tenantId, verifierId, dcqlQueryId) and points at a specific pinnedVersion of the shared query. The binding never copies the DCQL body or its history; that lives in the versioned DCQL store. Two verifiers binding the same dcqlQueryId at different versions consume two pointers into the same history, not two copies of the body.

enabled lets an operator disable a binding without deleting it; a disabled binding is rejected at authorization-request creation time with an ILLEGAL_ARGUMENT_ERROR. alias is a verifier-local short name for the query, useful when the same shared query is used in a UI that needs to label it per verifier (for example, "Age check (checkout)" vs "Age check (customer portal)" backed by the same age_verification query).

Bindings are soft-deleted (a deletedAt timestamp); live reads exclude soft-deleted rows.

How Resolution Uses the Binding

When the universal OID4VP backend creates an authorization request for a query_id, it calls DcqlQueryResolver.resolveForCreate(queryId, verifierId). When the VDX binding module is on the classpath, the resolver is VdxDcqlQueryResolver, which behaves as follows:

If verifierId is null, fall back to current-version resolution on the versioned store. This matches the EDK base behaviour and is what the Universal API uses when no per-verifier context is in scope.

If verifierId is not null, look up the live binding for (tenantId, verifierId, queryId). Reject with NOT_FOUND_ERROR if no binding exists. Reject with ILLEGAL_ARGUMENT_ERROR if the binding is disabled. Otherwise read the body for binding.pinnedVersion from the versioned store and snapshot (queryId, pinnedVersion) into the authorization session. The wallet's view of the request is therefore frozen at the pinned version regardless of what subsequent DCQL edits do.

The pinned version is also returned in ResolvedDcqlQuery.version, so the audit log and the authorization-session record both capture it.

REST API

Two adapters expose the binding surface. The main one is the verifier-scoped admin surface that lives alongside the Software Manager multi-verifier admin endpoints; the reverse-lookup adapter lives alongside the DCQL store admin endpoints.

Verifier-Scoped Bindings

Base path: /api/services/v1/oid4vp/verifiers/{verifierId}/dcql.

MethodPathDescription
GET/api/services/v1/oid4vp/verifiers/{verifierId}/dcqlList all DCQL bindings held by a verifier
POST/api/services/v1/oid4vp/verifiers/{verifierId}/dcqlBind a shared DCQL query to a verifier
GET/api/services/v1/oid4vp/verifiers/{verifierId}/dcql/{dcqlIdentifier}Read a single binding
PATCH/api/services/v1/oid4vp/verifiers/{verifierId}/dcql/{dcqlIdentifier}Immediately advance or roll back the binding's pinned version, toggle enabled, set alias
DELETE/api/services/v1/oid4vp/verifiers/{verifierId}/dcql/{dcqlIdentifier}Unbind a DCQL query from a verifier

The bind request:

POST /api/services/v1/oid4vp/verifiers/checkout-rp/dcql
Content-Type: application/json
Authorization: Bearer ...
{
"dcqlIdentifier": "age_verification",
"version": 4
}

version is optional; when omitted, the binding pins the DCQL query's current version at the moment of the call. Returns 201 Created with the resulting VerifierDcqlBinding.

The PATCH request supports partial updates:

{
"pinnedVersion": 5,
"enabled": true,
"alias": "Age check (checkout)"
}

A pinnedVersion update takes effect immediately. For changes that should land at a future time, use the scheduled-activation endpoints below instead.

Scheduled Activations

A scheduled activation is a request to move the binding's pinned version on a future date. It is projected over the EDK scheduler's scheduled_command table, not a dedicated activations table.

MethodPathDescription
POST/api/services/v1/oid4vp/verifiers/{verifierId}/dcql/{dcqlIdentifier}/activationsSchedule a future version activation
GET/api/services/v1/oid4vp/verifiers/{verifierId}/dcql/{dcqlIdentifier}/activationsList pending activations for a binding
DELETE/api/services/v1/oid4vp/verifiers/{verifierId}/dcql/{dcqlIdentifier}/activations/{activationId}Cancel a pending activation

The schedule request:

POST /api/services/v1/oid4vp/verifiers/checkout-rp/dcql/age_verification/activations
Content-Type: application/json
Authorization: Bearer ...
{
"targetVersion": 7,
"effectiveAt": "2026-06-01T00:00:00Z"
}

Returns 201 Created with a VerifierDcqlScheduledActivation:

{
"activationId": "act_01J...",
"verifierId": "checkout-rp",
"dcqlQueryId": "age_verification",
"targetVersion": 7,
"effectiveAt": "2026-06-01T00:00:00Z",
"status": "PENDING"
}

The activation's status is projected from the underlying scheduled-command lifecycle:

StatusMeaning
PENDINGScheduled, not yet executed
APPLIEDExecuted successfully; the binding's pinnedVersion was updated
CANCELLEDThe activation was cancelled before its effectiveAt
FAILEDExecution threw; the binding's pinnedVersion is unchanged and the error is in the audit log

When the scheduler fires at effectiveAt, it runs an internal command that calls the binding update path; from the binding's perspective this is identical to an operator calling PATCH .../dcql/{dcqlIdentifier} with pinnedVersion = targetVersion. The activation appears in the binding's history through the audit trail.

Cancelling a pending activation DELETEs the corresponding scheduled-command row; activations that have already executed cannot be cancelled (they appear with APPLIED status and are not part of any cancellable set).

Reverse Lookup

When you need to find every verifier in a tenant that is currently bound to a shared DCQL query (for example, before publishing a new version, to know who is going to be affected):

MethodPathDescription
GET/api/v1/oid4vp/dcql/{dcqlIdentifier}/verifiersList every verifier bound to a shared DCQL query, with the version each one pins

This adapter mounts under the DCQL admin base path (/api/v1/oid4vp) rather than the verifier admin base path, because it is the operation that crosses from "DCQL store admin" into "who is affected by this query". Returns a list of (verifierId, pinnedVersion, enabled, alias) summaries.

Why Two Bind Paths

The binding system shows up in two places for a reason. The verifier-scoped surface (/api/services/v1/oid4vp/verifiers/{verifierId}/dcql) is what a verifier admin uses: they manage their verifier's bindings. The reverse-lookup surface (/api/v1/oid4vp/dcql/{queryId}/verifiers) is what a tenant admin uses when they own the shared DCQL queries and want to understand or plan an organisation-wide change. The same data is exposed from both angles; the access policy on each surface is configured independently through the EDK authorization layer.

Persistence

Two backends are shipped, paralleling the DCQL store itself:

ModuleBackend
lib-data-store-oid4vp-dcql-binding-persistence-postgresPostgreSQL via SQLDelight
lib-data-store-oid4vp-dcql-binding-persistence-mysqlMySQL via SQLDelight

Both implement the VerifierDcqlBindingRepository interface from the public module. The schema is small (one verifier_dcql_binding table keyed by (tenant_id, verifier_id, dcql_query_id) with the mutable fields plus deleted_at); no shared infrastructure table is needed.

The scheduled-activations projection has no table of its own; it queries the EDK scheduler's scheduled_command rows whose target command id and arguments encode "advance binding X to version Y" and groups them by (verifierId, dcqlQueryId).

Wiring

Include the binding module alongside the versioned DCQL store. The VdxDcqlQueryResolver is bound with replaces = [EdkDcqlQueryResolver::class], so the verifier-aware resolver automatically takes the place of the tenant-only resolver in any assembly that has both on the classpath. Code that calls DcqlQueryResolver.resolveForCreate(...) keeps the same signature.