Running the data plane without a database
By default every DVARA component talks to PostgreSQL directly. Since 1.7.0 a data-plane pod can run with no datasource at all: it serves configuration from a signed bundle it fetches from the control plane, and writes audit and telemetry to a local spool that ships back to flightdeck.
This is opt-in and reversible. Nothing changes on upgrade — a pod with neither switch set behaves exactly as 1.6.0 did.
Why you would
Two properties, and the second is the one operators usually care about first.
Blast radius. An attached gateway pod holds a PostgreSQL credential. That pod is also the process holding every workspace's provider API keys and PII encryption keys, so a compromise of it is a compromise of the database. A decoupled pod holds no database credential at all — a stronger statement than any set of GRANTs, because there is nothing to grant.
Availability. An attached pod's tolerance for a PostgreSQL outage is its config cache TTL. Past that, authentication lookups fall through to a dead database and the data plane stops serving. A decoupled pod keeps serving from the bundle it already holds, for as long as the outage lasts.
Config reads are already served from an embedded cache, so a licensed pod issues no config reads per request even when attached. What the bundle removes is the dependency, not the queries.
The two switches
They are deliberately separate, and the useful state is the one where only the first is set.
| property | what it does | |
|---|---|---|
| Config reads | dvara.data-plane.serve-from-bundle | serve configuration from the bundle instead of PostgreSQL |
| Audit + telemetry writes | dvara.llm-gateway.local-store.path | spool writes to a local SQLite file instead of PostgreSQL |
Setting dvara.data-plane.control-plane.base-url without serve-from-bundle puts the pod in shadow mode: it fetches, verifies and snapshots bundles, and continuously compares them against what PostgreSQL says — while still serving from PostgreSQL.
That is the only state in which the divergence metric is evidence for the cut-over rather than a report on one you already took. Run it there first.
What a fully decoupled pod needs
Three things, and the pod refuses to start if it has some but not all of them:
DVARA_DATA_PLANE_SERVE_FROM_BUNDLE=true # where config is read from
DVARA_LLM_GATEWAY_LOCAL_STORE_PATH=/var/lib/dvara/spool.db # where writes are spooled
DVARA_DATA_PLANE_CONTROL_PLANE_BASE_URL=https://flightdeck… # where the spool drains to
DVARA_DATA_PLANE_CONTROL_PLANE_PUBLIC_KEY=<base64 Ed25519> # what verifies the bundle
Drop spring.datasource.url only once those are set. If any are missing, startup fails and names the ones you are missing:
========================================
STARTUP FAILED: no database, and no
complete replacement for one
========================================
dvara.llm-gateway.local-store.path=... (where audit and telemetry are written)
There is deliberately no single "run without a database" flag. Each missing piece fails silently in production — a dropped audit trail is invisible until someone asks for it — so the posture must not be half-configurable.
local-store.path set, no datasource, and no control-plane.base-url means spooling evidence into a queue with no consumer. That is refused at startup, because it is the exact failure the spool exists to prevent — arrived at by configuration instead of by an outage.
The config bundle
flightdeck materializes every workspace, key, route, policy, credential, price, budget, schema, prompt, plugin, MCP server and A2A agent into one document, canonicalizes it, and signs it with Ed25519. Pods fetch it, verify the signature against a pinned public key, and swap it in atomically.
Read the public key from GET /v1/admin/config/bundle/signing-key (owner-only) rather than out of a startup log. If you set no signing key, flightdeck generates one once and persists it, so a zero-config install still gets a stable key.
Rotating the signing key is a rolling change, not a flag day. A pod accepts public-key plus a list of additional-public-keys: add the incoming key fleet-wide first, then switch flightdeck to sign with it, then promote it and drop the old one. At no point does the control plane sign with a key some pod does not yet accept.
Cold start during an outage
Set dvara.data-plane.control-plane.snapshot-path to a durable path and the pod persists every bundle it verifies, reloading it at boot before the first fetch. Without it, a pod that restarts while the control plane is down has nothing to serve.
The snapshot is written atomically and re-verified on load, so a crash cannot leave a torn file and a tampered one is rejected.
A brand-new pod, on a fresh volume, with the control plane down and no snapshot, cannot serve. There is nowhere for it to get configuration from.
Staleness is reported, never enforced
A pod serving old configuration surfaces on /actuator/gateway-status once the bundle passes staleness-warn-seconds (default one hour). It is an alarm, not a cut-off — a pod is never stopped from serving because its config is old, because that turns a control-plane outage into a data-plane outage.
The audit spool
Writes land in local SQLite and ship to flightdeck in idempotent batches. The chain is per-pod: each pod signs its own HMAC chain, which is what makes the decoupling a strengthening of the tamper-evidence claim rather than a weakening of it.
Audit cannot be shed. The spool is bounded, and when it fills, usage, cost and observability rows are dropped — never audit events. That asymmetry is the point of the tiering: telemetry is reconstructable or expendable and an audit event is neither.
PII degrades visibly. Past a per-workspace high-water mark the pod stops minting PII tokens rather than discarding them, and applies the workspace's pii.degraded-action — BLOCK by default. A workspace that asked for redaction is refused rather than quietly having its data sent upstream unredacted.
Drain on shutdown. On SIGTERM the pod flushes in-process, bounded by shutdownDrainSeconds (default 20). Every routine path — rolling update, scale-in, node drain — is therefore graceful, and the volume covers only the ungraceful ones. Your termination grace period must exceed that value, or the flush is cut off mid-way.
Revocation still works during an outage
If configuration only arrived on the bundle, a control-plane outage would mean no revocation was possible at all. A second, minimal channel carries a signed deny-list — API keys, workspaces, sessions and credentials — polled every ten seconds and alarmed at five minutes rather than the bundle's hour.
It never hard-refuses: a pod that cannot reach the channel keeps serving and keeps the revocations it already holds. Only revocations issued during the outage are delayed.
deny-list-url defaults to the same control plane as the bundle, which is convenient and gives up the independence the channel exists for. Point it at a mirror if the guarantee matters to you.
What changes when you turn it on
- Config writes on that pod start failing. A signed bundle is read-only; an admin write returns an error rather than reporting a success that changed nothing. Make configuration changes on
flightdeck. - The config cache stands down. A held bundle is already an authoritative cache with its own version; a TTL in front of it would only delay changes.
- Workspace-tier quota enforcement stands down if the pod cannot count month-to-date usage. It fails open — the request is served and a warning is logged once per plane. A hosted fleet should not cut over until a fleet-wide counter is in place; self-hosted installs have no tiers and are unaffected.
On Kubernetes
Set dataPlane.controlPlane.baseUrl and the chart wires the rest. Two things to know:
persistence.enabled: true turns the workload into a StatefulSet, because a Deployment cannot give each replica its own read-write volume. That is a change of kind: updates become ordered, pods get stable ordinals, and zonal volumes pin a pod to its zone. Off by default for that reason.
The /internal certificate's CN is the pod's scope — CN=gateway:eu may fetch the gateway:eu bundle and nothing else. Get it wrong and the pod authenticates perfectly, then receives 403 for its own scope, which is a confusing failure from both ends.
What this does not do
- It does not remove PostgreSQL.
flightdeckstill needs it — it becomes the only client. - It does not change what the gateway serves, or how governance behaves.
- It is not required. An attached install is fully supported and is still the default.