---
title: 'ADR 0013: Deployment model'
description: Stateless planes, one image, forward-only migrations, and enforcement mode stored as data rather than configuration.
label: '0013 Deployment model'
---

## 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](/operate/deployment) and
[Database](/operate/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](/architecture/adr/0002-gateway-architecture), 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](/operate/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](/operate/configuration) and
[Troubleshooting](/operate/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.

## Related

- [Deployment](/operate/deployment)
- [Run the stack](/operate/run-the-stack)
- [Upgrades](/operate/upgrades)
- [Gateway architecture](/architecture/adr/0002-gateway-architecture)
