---
title: Run the stack
description: Three processes, what each is responsible for, how to check they are healthy, and how to scale them independently.
---

## The three processes

| Process | Entry point | Serves | Reached by |
| --- | --- | --- | --- |
| Control plane | `apps/control-plane/src/server.ts` | `/v1/*` | People, console, CLI, your CI |
| Gateway | `gateway/src/server.ts` | `/gateway/v1/*` | **Agents only** |
| Console | `apps/console` | The browser app | People |

Plus a background worker, `apps/control-plane/src/worker.ts`, which runs the job
queues: webhook fan-out and delivery, usage rollups, risk baseline recomputation,
notifications, and reclaiming abandoned jobs.

:::security The split is a security boundary, not a packaging choice
The gateway is the only process agents can reach. It has a smaller HTTP surface,
it accepts no human credentials, and it can be scaled, network-isolated and
hardened independently of the management API.

Do not merge them. Do not expose `/v1/*` on the same ingress agents reach.
:::

## Health

```bash
curl -s http://localhost:4000/health/live
curl -s http://localhost:4000/health/ready
curl -s http://localhost:4010/health/live
curl -s http://localhost:4010/health/ready
```

| Endpoint | Means | Use as |
| --- | --- | --- |
| `/health/live` | The process is up and the event loop is responsive | Liveness probe |
| `/health/ready` | Dependencies are reachable — the database, in particular | Readiness probe |

`/health/ready` genuinely checks the database. If the database is gone,
readiness goes red rather than reporting healthy and then failing every request.

:::note This was a real bug worth knowing about
The gateway's readiness check once stayed green with no database, because the
embedded driver silently booted a fresh empty database on the next query after
being closed. A "healthy" gateway with an empty database is worse than a red
one, because your load balancer keeps sending it traffic. The driver now treats
close as terminal, and readiness reflects reality.
:::

## Scaling

**Both planes are stateless.** Session state and the job queue live in the
database. Run as many instances as you need behind a load balancer.

**Scale them independently.** They have completely different load profiles: the
gateway handles agent traffic, which is high volume and latency sensitive; the
control plane handles human and CI traffic, which is low volume and
bursty. Typical ratio is 3:1 gateway to control plane.

**Run at least one worker.** Job claiming is tenant-safe and guarded against
overlapping poll ticks, so several workers can run concurrently without
duplicating work. With no worker, webhooks never deliver and usage never rolls
up — and nothing visibly breaks, which is the annoying part.

:::note Another real bug worth knowing about
Every job claim once hit the repository's tenant-scope guard. The error was
swallowed by the poll loop, and 67 jobs wedged every queue permanently with no
alert. Claiming now carries an explicit cross-tenant justification, abandoned
jobs are reclaimed and dead-lettered after repeated failure, and overlapping
ticks are prevented.

Monitor queue depth. A silently stalled queue is the failure mode that does not
page anyone.
:::

**Redis is optional.** Leave `REDIS_URL` empty for the in-process store, which
is correct for a single-node run. Set it for multi-node, so rate limiting and
nonce tracking are shared — otherwise each instance has its own view and the
effective rate limit is multiplied by your instance count.

## Starting it

Development:

```bash
npm run dev                # all three, from the root .env
npm run dev:control-plane
npm run dev:gateway
npm run dev:console
```

Production:

```bash
npm run build
node dist/control-plane/server.js
node dist/gateway/server.js
node dist/control-plane/worker.js
```

The console builds to static assets. Serve them from any static host or CDN; it
talks to the control plane from the browser, so `CORS_ALLOWED_ORIGINS` must
include wherever you serve it from.

## Ordering during a deploy

1. **Migrations.** `npm run migrate`. Migrations are forward-only and designed to be safe against the previous version, so this runs before any process restarts.
2. **Control plane.** Rolling.
3. **Gateway.** Rolling.
4. **Worker.** It can restart at any point; in-flight jobs are reclaimed.
5. **Console.** Static assets; publish last.

Gateway last among the request-serving processes, so the control plane is
already on the new schema when the gateway starts depending on it.

## Logs

Structured JSON through pino. `LOG_LEVEL` controls verbosity;
`info` is the production default.

Every line carries `requestId` and `traceId`, so a `request_id` from an error
response finds every log line that request produced.

Redaction is applied **on the way in**, not on the way out. There is no code
path where a secret is written to a log and later filtered.

:::warning Do not raise the log level to debug in production and leave it
`debug` logs request and response shapes. Redaction still applies to known
secret shapes, but the volume alone will cost you, and a log retained for 90
days is a much larger surface than one retained for 7.
:::

## Metrics

`METRICS_ENABLED=true` exposes Prometheus metrics at `/metrics`. See
[Observability](/operate/observability) for what to alert on.

## What each process needs

| | Control plane | Gateway | Worker |
| --- | --- | --- | --- |
| Database | yes | yes | yes |
| `VEEFORE_DATA_ENCRYPTION_KEY` | yes | yes | yes |
| `VEEFORE_SIGNING_PRIVATE_KEY` | yes | yes | — |
| `VEEFORE_SESSION_SECRET` | yes | — | — |
| `VEEFORE_AUDIT_CHAIN_SECRET` | yes | yes | yes |
| Outbound egress | for webhooks | for tool execution | for webhooks |
| Inbound from agents | **no** | yes | no |
| Inbound from people | yes | no | no |

The gateway does not need `VEEFORE_SESSION_SECRET`, because it never issues a
human session. Keep it out of the gateway's environment — a secret a process
does not need is a secret it should not have.

## Related

:::cards

:::card Configuration | /operate/configuration
The settings that matter most.
:::

:::card Deployment | /operate/deployment
Containers, Kubernetes, rollout.
:::

:::card Observability | /operate/observability
Metrics, traces and alerts.
:::

:::card Request path | /architecture/request-path
What happens between the processes.
:::

:::
