Deployment
Containers, Kubernetes, Helm and Terraform — plus the network topology that makes the plane split worth having.
The repository ships deployment definitions under infrastructure/, validated
by npm run infra:validate as a stage of npm run verify.
infrastructure/docker/Dockerfile |
Multi-stage build for all three Node processes |
infrastructure/docker/docker-compose.yml |
Single-node stack with PostgreSQL |
infrastructure/kubernetes/base.yaml |
Namespace, config, secrets, service accounts |
infrastructure/kubernetes/control-plane.yaml |
Deployment, service, probes, HPA |
infrastructure/kubernetes/gateway.yaml |
Deployment, service, probes, HPA |
infrastructure/helm/veefore/ |
Chart and values |
infrastructure/terraform/ |
Database, networking, secret references |
#Build
bashnpm run build # backend and console
npm run build:backend
npm run build:console
bashdocker build -f infrastructure/docker/Dockerfile -t veefore:1.0.0 .
One image, three entry points. The processes share almost all their code, and a single image means one thing to scan, sign and promote.
bashnode dist/control-plane/server.js
node dist/gateway/server.js
node dist/control-plane/worker.js
#Network topology
This is the part that makes the plane split worth having.
text Internet / your VPC
│
┌─────────────────┴─────────────────┐
│ │
ingress: console ingress: gateway
+ control plane /gateway/v1/* only
/v1/* only │
│ │
people, CLI, CI agents
│ │
└───────────────┬───────────────┘
│
PostgreSQL (private)
Separate ingresses. Do not merge them.
The gateway is the only process agents reach, and it must expose only
/gateway/v1/* and /.well-known/veefore-platform-key.
Exposing /v1/* on the ingress agents can reach undoes the design. The control
plane is where policies are activated and credentials are registered; it has no
business being reachable from wherever your agents run.
Correspondingly, do not expose /gateway/v1/* on the human-facing ingress.
There is no reason for a browser to reach it.
The database is private to both planes. Nothing outside the cluster or VPC reaches it.
#Kubernetes
bashkubectl apply -f infrastructure/kubernetes/base.yaml
kubectl apply -f infrastructure/kubernetes/control-plane.yaml
kubectl apply -f infrastructure/kubernetes/gateway.yaml
The probes are the important part of those manifests:
yamllivenessProbe:
httpGet: { path: /health/live, port: http }
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet: { path: /health/ready, port: http }
initialDelaySeconds: 5
periodSeconds: 5
/health/ready genuinely checks the database, so a pod with a broken connection
leaves rotation instead of serving errors.
Both planes are stateless — session state and the job queue are in the database — so horizontal scaling needs no coordination. Scale them independently: the gateway sees high-volume, latency-sensitive agent traffic; the control plane sees low-volume, bursty human and CI traffic. 3:1 is a reasonable starting ratio.
Run at least one worker replica. Job claiming is tenant-safe and guarded against overlapping poll ticks, so several can run concurrently. With none, webhooks never deliver and usage never rolls up, and nothing visibly breaks.
#Secrets
bashkubectl create secret generic veefore-keys \
--from-literal=VEEFORE_DATA_ENCRYPTION_KEY=… \
--from-literal=VEEFORE_SIGNING_PRIVATE_KEY=… \
--from-literal=VEEFORE_SIGNING_KEY_ID=… \
--from-literal=VEEFORE_SESSION_SECRET=… \
--from-literal=VEEFORE_AUDIT_CHAIN_SECRET=…
Better: an external secrets operator pulling from your secret manager, so rotation does not mean editing a manifest.
Give each process only the secrets it needs
The gateway does not issue human sessions, so it does not need
VEEFORE_SESSION_SECRET. The worker does not sign passports, so it does not
need VEEFORE_SIGNING_PRIVATE_KEY.
A secret a process does not need is a secret that process should not have — it is one more place a compromise can read it from. The table in Run the stack lists what each one requires.
#Rollout order
- Migrations, as a job that completes before anything restarts. Forward-only and safe against the previous version.
- Control plane, rolling.
- Gateway, rolling.
- Worker. Any time; in-flight jobs are reclaimed.
- Console static assets.
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
Deploying does not change any environment's mode. It persists in the database
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.
This is deliberate: a mode that could be changed by a deploy could be changed by a misconfigured pipeline.
#TLS
Terminate at your ingress. Both planes speak HTTP behind it.
veefore doctor reports a control plane or gateway reachable over plaintext
HTTP on a non-loopback host as a finding. Egress has the same rule enforced
rather than reported: a tool endpoint must be HTTPS unless its host is loopback.
#Terraform
infrastructure/terraform/ provisions the database, the networking and the
secret references. It does not provision the application — that is Helm or raw
manifests, so you can move the application forward without touching
infrastructure state.
bashcd infrastructure/terraform
terraform init
terraform plan
#Validate before you ship
bashnpm run infra:validate
Checks that the container, Kubernetes, Helm and Terraform definitions are well
formed. Part of npm run verify.
#Scaling signals
| Signal | Action |
|---|---|
| Gateway p99 decision latency rising | More gateway replicas |
Database connections near max_connections |
PgBouncer, or lower DATABASE_POOL_MAX |
| Job queue depth growing | More worker replicas |
| Rate limits firing unexpectedly across instances | Set REDIS_URL so limits are shared |
That last one is a real trap: without Redis, each instance keeps its own rate limit and nonce state, so your effective limit is multiplied by your instance count and nonce replay protection is weaker than you think.