Veefore docs

ADR 0013: Deployment model

Stateless planes, one image, forward-only migrations, and enforcement mode stored as data rather than configuration.

#Context

Veefore has to be deployable by a team that is not the team that wrote it, into infrastructure they already run, with a rollout that does not require a maintenance window. And it must be hard to deploy in a way that quietly weakens the security model.

Four decisions follow from that.

#Decision

#Stateless request-serving processes

Session state and the job queue live in PostgreSQL. Both planes can be scaled horizontally with no coordination, no sticky sessions and no shared cache.

#One image, three entry points

bash
node dist/control-plane/server.js
node dist/gateway/server.js
node dist/control-plane/worker.js

The processes share almost all their code. One image means one artefact to scan, sign and promote.

#Forward-only migrations

No down-migrations. Each migration is written to be safe against the previous application version, which is what makes a rolling restart possible:

text
1. migrations          (job, completes first)
2. control plane       (rolling)
3. gateway             (rolling)
4. worker              (any time)
5. console assets      (last)

Gateway after the control plane, so the control plane is already on the new schema when the gateway starts depending on it.

#Enforcement mode is data, not configuration

It lives in the database on the environment row, and changes only through POST /v1/environments/{id}/gateway-mode or veefore deploy — with a reason, step-up authentication, and a CRITICAL audit event if it reduces enforcement.

#Configuration is environment-only

No runtime configuration file, no settings table. Validated at startup; an invalid value stops the process rather than degrading it.

#Honest readiness

/health/ready genuinely checks the database on both planes.

#Alternatives

Enforcement mode as an environment variable. Would make it deployable with the rest of the configuration, and it would mean a misconfigured pipeline can reduce enforcement. It would also lose the reason, the step-up and the audit event.

The whole asymmetry — tightening is routine, loosening leaves a mark — depends on the mode being data that changes through an audited operation. Rejected.

Reversible migrations. The conventional expectation. A down-migration that drops a column drops the data in it, and for an audit trail that is deletion rather than rollback.

The route to undoing a schema change is a new forward migration, which leaves a record. Application rollback is separate and works, because migrations are safe against the previous version.

Separate images per process. Smaller images, and three artefacts to scan, sign, promote and keep in step. The shared packages are the bulk of the code anyway, so the size saving is modest and the drift risk is real.

Stateful sessions in process memory. Faster session lookup. Requires sticky sessions, breaks on a rolling restart, and makes horizontal scaling a coordination problem.

A runtime configuration file or settings table. Lets an operator change behaviour without a restart. It also means the running configuration can disagree with the process that booted, and "what is this deployment actually configured as" becomes a question with two answers.

Liveness-only health checks. Simpler. A process that is live with a broken database connection stays in the load balancer's rotation and serves errors, which is worse than being pulled out.

#Consequences

PostgreSQL is on the critical path for session lookup. Acceptable: it is already on the critical path for every authorization decision, and fail-closed means a database outage refuses protected actions regardless.

DATABASE_POOL_MAX is per process instance, and scaling out multiplies connections. Three control plane, two gateway and one worker at 20 each is 120. Size max_connections for the total, and consider PgBouncer past a handful of instances. A recurring operational surprise, so it is stated in Deployment and Database.

Migrations must be written to be backwards-safe, which constrains what a migration may do. Adding a nullable column is fine; renaming one in a single migration is not. npm run migrate:check applies every migration to a throwaway database and verifies it comes out clean.

Editing an applied migration is a silent divergence. The edit applies only to new environments, and nothing indicates the drift. The rule is to add a new one, and migrate:check is the gate.

Separate ingresses are a deployment responsibility the manifests cannot enforce. Exposing /v1/* where agents can reach it undoes ADR 0002, and no Kubernetes manifest can prevent it. Stated prominently and checked in review.

Each process gets only the secrets it needs. The gateway does not issue human sessions, so it does not need VEEFORE_SESSION_SECRET. A table of what each process requires is in Run the stack.

A database backup alone is not restorable. Credential material is sealed with VEEFORE_DATA_ENCRYPTION_KEY and the chain is keyed with VEEFORE_AUDIT_CHAIN_SECRET, neither of which is in the database. Both must be backed up alongside it, and the restore must be tested with both.

Console configuration is baked in at build time. VITE_* values are compiled into the bundle, so changing the API URL requires a rebuild and redeploy — restarting a server does nothing. A real source of confusion, so it is called out in Configuration and Troubleshooting.

At least one worker must run, and nothing visibly breaks without it. Webhooks stop delivering and usage stops rolling up, silently. Monitor veefore_job_queue_depth — a stalled queue is the failure mode that does not page anyone.

Identity, authority, policy and enforcement for autonomous AI agents.