Per-Tenant Configuration
The EDK has one layered configuration model that every service uses. There are three scopes:
- App scope. Deployment-wide config from YAML, environment variables, and cloud config providers.
- Tenant scope. Per-tenant config stored in the database and read at runtime.
- Principal scope. Per-user overrides. The narrowest scope, rarely used outside specialised flows.
The runtime combines the three, and the rule is narrower wins: a property defined at both the app and tenant level resolves to the tenant value inside a request bound to that tenant, and to the app value otherwise. This page covers the tenant scope; the app scope is covered in the Roles and Topology configuration page.
What Per-Tenant Config Holds
A per-tenant property is a dotted key (oauth2.servers.default.issuer, oid4vci.issuer.signing_key_alias, webhook.dispatch.timeout_ms) bound to one tenant. Two flavours:
- Non-secret. The value is stored directly. Examples: a tenant-specific cache TTL, a feature toggle, a per-tenant URL base.
- Secret-bearing. Only a reference into the configured secret backend is stored; the secret value itself lives in the backend, never in the database. Examples: an IdP client secret, an SMTP password, a webhook signing key.
Whether a property is secret-bearing is determined by the config domain that owns it, not by the caller; secret writes are routed to the secret backend automatically.
Writing Tenant Config
Tenant administrators do not write raw config rows. Each configuration domain exposes a typed admin API that knows its own schema, its secret-bearing fields, and its validation rules: the OID4VCI issuer config (credential designs, attribute suppliers), the OID4VP verifier config (DCQL, trust), the AS config (servers, clients, federation providers), the DID method config, the integration registry, the webhook config, and the tenant admin itself for tenant-scoped infrastructure overrides. Behind each, the endpoint writes the non-secret value directly and routes the secret half through the secret backend, leaving only the reference in tenant config.
There is no raw key/value or JSONB tenant-config endpoint, by design. A raw endpoint removes type safety, makes secret classification unenforceable, and lets an administrator write values the runtime is unprepared for. Every config domain has a typed surface instead.
Reading Tenant Config
The runtime reads through the scope chain: principal scope (usually empty), then tenant scope, then app scope. The narrowest scope that has the property wins. For a request bound to tenant acme, a read checks the principal scope, then acme's tenant config, then falls through to the app scope. Typed config blocks bind at the appropriate scope the same way. Tenant reads are cached so a per-tenant lookup stays off the hot path.
Secret References
A secret reference is a typed URI naming a backend and a key path:
secret:vault:edk/tenant-acme/idp-client-secret
secret:aws:edk/acme/webhook-hmac
secret:azure:edk-vault/acme/oauth-client-secret
secret:k8s:acme-secrets/idp-secret
The same ${secret:...} syntax works in YAML and environment variables. At read time the runtime resolves the reference through the configured backend, caches the value for a short TTL, and returns it. The reference itself is non-secret: storing references in tenant config is safe even if the database is exfiltrated, because the actual secret lives only in the backend.
When a typed admin command stores a secret-bearing property, it writes the secret value to the backend under a path derived from the tenant and property, stores the resulting reference, and records an audit event with the reference (never the plaintext) and the writer principal. The backend selection is deployment-wide; see Application Tenant and Bootstrap for choosing it.
Propagation Across Replicas
A tenant admin updating config on one replica must not be silently overridden by another replica serving a stale cached value. After a config write commits, the change propagates over the shared event channel to every replica, which invalidates the affected entry (or the whole tenant scope for a broad change). The next read repopulates from the database. A cache TTL covers a missed notification, so a change is visible everywhere at most one TTL window later. A deployment standardised on a different event bus (Kafka, NATS, a service mesh's eventing) can substitute its own propagation mechanism; the EDK default uses Postgres LISTEN/NOTIFY.
The Scope Chain in Practice
A few examples that show how scopes compose:
- Per-tenant signing key alias. Override the default issuer signing alias for one tenant with
oid4vci.issuer.signing_key_alias = "acme-custom-alias". Onlyacme's issuance reads it; other tenants keep their own or the default. - Per-tenant webhook timeout. The default is 5s. Set
webhook.dispatch.timeout_ms = "30000"for a tenant with a slow consumer; other tenants stay at 5s. - Per-tenant SMTP. A tenant can ship its owner invitation emails through its own SMTP server by setting the SMTP host and a credentials reference in its tenant config; the email service consults the tenant scope when picking a transport.
- Per-tenant external IdP client secret. Stored as a secret reference and resolved at the federation handshake, not at config-read time.
- Per-tenant audit signing. Enable audit checkpoint signing for one tenant with
audit.events.signing.enabled = "true"without enabling it deployment-wide.
Auditing Config Changes
Every typed admin command emits a structured audit event when it writes config or a secret. The event carries the tenant id, the acting principal, the operation, the property keys touched, and (for secret writes) the new reference, never the plaintext. The audit stream is the operator's "who changed what when" record and can replicate to an external SIEM; see Operations.