Veefore docs

Database

Migrations, connection pooling, row-level security, backups, and what to restore when.

One PostgreSQL database, shared by the control plane, the gateway and the worker. PostgreSQL 14 or newer.

#Migrations

Nine migrations, applied in order, forward-only.

Covers
0001 Tenancy and people — organizations, workspaces, environments, members, roles
0002 Agents — identities, keys, generations, passports, capability grants
0003 Tools and credentials — the registry, sealed material, leases
0004 Policy and delegation — policies, versions, test cases, bindings, delegations
0005 Risk, approval and trust — signals, events, approvals, evidence
0006 Actions and audit — action records, stage results, the hash-chained trail
0007 Security and protocols — incidents, emergency controls, MCP, A2A, registry
0008 Platform — webhooks, API keys, usage, plans, subscriptions, SSO, SCIM
0009 Row-level security — the policies, applied when enabled
bash
npm run migrate           # apply
npm run migrate:check     # apply to a throwaway database and verify it comes out clean

migrate:check is the CI gate. It proves every migration applies to an empty database — which is the thing that breaks when someone edits an already-applied migration instead of adding a new one.

Never edit an applied migration

Add a new one. An edited migration has already run on every deployed environment, so the edit silently applies only to new ones and your environments diverge with nothing to indicate it.

A real bug in the migration recording

node-postgres returns an array of results for multi-statement SQL. The driver was reading .rows off the array, got undefined, and threw — after the DDL had already committed. The schema was applied but not recorded, so the next run tried to apply it again and failed on an existing table.

The driver now normalises multi-statement results. If you ever see a migration that "failed" but whose tables exist, that class of bug is what it looks like.

#Pooling

ini
DATABASE_POOL_MAX=20
DATABASE_STATEMENT_TIMEOUT_MS=15000

DATABASE_POOL_MAX is per process instance. Three control plane instances, two gateway instances and one worker at 20 each is 120 connections. Size PostgreSQL's max_connections for the total, and consider PgBouncer in transaction mode once you are past a handful of instances.

DATABASE_STATEMENT_TIMEOUT_MS bounds a single statement. The gateway is on the latency path for every protected action, so a query that runs away must be cut off rather than holding a connection.

#Row-level security

ini
DATABASE_ENABLE_RLS=true

Turns on PostgreSQL row-level security as a second, independent isolation layer beneath the application-level tenant scoping.

Enable it in production

The application layer already requires a tenant scope on every query and refuses without one. RLS exists so that a bug in that layer — a query that forgets its scope, a new repository method that does not inherit the guard — does not become a cross-tenant data leak.

Two independent layers, where either alone would be sufficient. That is the point of defence in depth: you do not have to be confident that the first layer is perfect.

See Tenant isolation.

#The embedded driver

Veefore ships PGlite, an embedded PostgreSQL, selected by PGLITE_DATA_DIR when DATABASE_URL is absent.

Use it for: isolated unit and integration tests, where a private per-process database is exactly what you want.

Do not use it for: running the stack.

PGlite is per-process

Each process gets its own private copy. The control plane, the gateway, the worker and any seed script would see none of each other's writes.

The symptom is not an error. It is an agent you just created not existing, a policy you just activated not applying, and a login that works in one process and fails in another. Set DATABASE_URL.

Related: close() on the embedded driver is now terminal. It previously booted a fresh empty database on the next query, which made the gateway's readiness check stay green with no data behind it.

#Backups

Standard PostgreSQL practice, with one Veefore-specific consideration.

bash
pg_dump --format=custom --no-owner --no-privileges veefore > veefore.dump

A database backup is not a complete backup

Credential material in the dump is sealed with VEEFORE_DATA_ENCRYPTION_KEY, which is not in the database. Restore the dump without that key and every credential is unreadable.

The audit chain has the same property with VEEFORE_AUDIT_CHAIN_SECRET: restore without it and you can read the events but you cannot verify integrity.

Back up the four keys alongside the database, in your secret manager, and test a restore that uses both. A backup you have not restored is a hypothesis.

Point-in-time recovery is worth configuring. The audit trail is append-only in practice, so PITR gives you a consistent view of decisions as of any moment — which is the thing an investigation needs.

#Restoring

  1. Restore the database.
  2. Ensure the four keys match the ones in use when the backup was taken.
  3. npm run migrate — bringing a restored older snapshot up to the current schema.
  4. veefore audit verify — confirms the chain survived the round trip.
  5. Start the processes.

Step 4 is the one people skip and it is the one that tells you whether the restore is sound.

#Retention and growth

The two tables that grow without bound are actions and audit_events, roughly proportional to protected action volume.

Audit retention is configurable, and changing it requires audit:configure_retention — step-up gated, writing a CRITICAL audit event. Shortening retention destroys evidence, so a request to "clean up old audit data" leaves a mark explaining who asked and why.

Export before you shorten:

bash
veefore audit export --format csv \
  --reason "Pre-retention-change archive" --out archive.csv

The export is watermarked and hashed, so the archive is verifiable later.

#Health

/health/ready on both planes checks database reachability. A readiness probe pointed at it will pull an instance out of rotation when its database connection is gone, rather than letting it serve errors.

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