Veto/docs
Meow Gateway

Self-host runbook

Operating a self-hosted meow-gateway — signing keys, storage, backup, upgrades, key rotation, disaster recovery.

The gateway is designed to run without any Veto-hosted service. This runbook covers day-2 operations.

Install options

MethodCommandWhen to use
npxnpx @veto/meow-gateway startFirst run, laptop eval
Homebrewbrew install plawio/tap/veto-meow-gatewaymacOS long-term install
Dockerdocker run ghcr.io/plawio/veto-meow-gateway:<version>Production

Signing keys

The gateway signs capsules with Ed25519. Keys live in $KEYS_DIR (default ./veto-keys/) as a PKCS8 PEM pair. The directory is created with chmod 600 — the gateway refuses to start if the private key is group- or world-readable.

npx @veto/meow-gateway keygen --kid 2026-q2

Rotate keys quarterly. The JWKS endpoint serves both the active key AND the previous one for 30 days so in-flight capsules keep verifying:

# Rotate:
npx @veto/meow-gateway keygen --kid 2026-q3 --force
# Old kid stays in JWKS for the overlap window.

Invariant: max(capsule_ttl) × 10 ≤ JWKS overlap. With the default 24h capsule ceiling and 30d overlap, you have 300× headroom.

Storage

Default storage is SQLite ($STORAGE_DIR/store.sqlite) + append-only NDJSON for receipts ($STORAGE_DIR/receipts.ndjson). The gateway takes an exclusive OS-level lock on the SQLite file; two gateway processes sharing a directory refuse to boot.

Backup

# Online backup — SQLite's native .backup is atomic:
sqlite3 $STORAGE_DIR/store.sqlite ".backup /secure/backup/store-$(date +%s).sqlite"

# Receipts NDJSON is append-only and safe to rsync live:
rsync -av $STORAGE_DIR/receipts.ndjson /secure/backup/

Restore

# Stop the gateway first.
cp /secure/backup/store-<ts>.sqlite $STORAGE_DIR/store.sqlite
cp /secure/backup/receipts.ndjson   $STORAGE_DIR/receipts.ndjson
# Restart. The gateway verifies chain continuity on first read.

Upgrading

The protocol is versioned (veto.capsule/1). Minor gateway upgrades are drop-in:

# npm:
npm i -g @veto/meow-gateway@latest

# Docker:
docker pull ghcr.io/plawio/veto-meow-gateway:latest

Schema migrations run idempotently on boot. The gateway refuses to start if the SQLite file was written by a NEWER version than the binary — never silently downgrade.

Connecting to veto.so

npx @veto/meow-gateway connect

Opens a browser OAuth to auth.veto.so, writes VETO_PLATFORM_URL and VETO_API_KEY to your .env, and flips STORAGE_MODE=hybrid. Your local SQLite stays canonical; Convex gets a best-effort mirror for dashboard freshness.

Disconnect by unsetting the two env vars and flipping STORAGE_MODE=local. The gateway re-reads the env on SIGHUP; no data loss.

Environment reference

VariableDefaultNotes
STORAGE_DIR./veto-storageSQLite + NDJSON lives here
KEYS_DIR./veto-keysPKCS8 PEM + JWKS JSON
VETO_SIGNING_KIDdefaultActive signing kid
MEOW_GATEWAY_PORT3005HTTP listener port
LISTEN_HOST127.0.0.1Bind to 0.0.0.0 for containerized deploys
MEOW_MODEmocklive once you've completed Meow OAuth
VETO_CAPSULE_MAX_TTL_SECONDS86400Hard ceiling; <= jwks_overlap / 10
VETO_HITL_WEBHOOK_URL<unset>POST destination for approval.required
VETO_HITL_WEBHOOK_SECRET<unset>HMAC-SHA256 secret for X-Veto-Signature
LOG_LEVELinfodebug shows per-rule policy eval

Health + readiness

  • GET /health — liveness probe; 200 iff process responsive
  • GET /ready — readiness probe; 503 if storage or signing unavailable
  • GET /.well-known/veto-keys.json — JWKS; verifiers download on first use and cache

Disaster recovery

The decision receipt chain is the authoritative audit trail. If the SQLite file is lost but the NDJSON survives, you can rebuild approximate state; if both are lost, capsules already signed will still verify against the JWKS endpoint (the chain restarts from genesis on the new install).

Protocol guarantees:

  • Every receipt hash-links to the previous one
  • The genesis receipt's prev_receipt_hash is sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 (SHA-256 of the empty byte string)
  • Merkle root is recomputed every 1024 receipts — use /v1/receipts/:entity/verify to recheck the full chain