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
| Method | Command | When to use |
|---|---|---|
| npx | npx @veto/meow-gateway start | First run, laptop eval |
| Homebrew | brew install plawio/tap/veto-meow-gateway | macOS long-term install |
| Docker | docker 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-q2Rotate 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:latestSchema 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 connectOpens 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
| Variable | Default | Notes |
|---|---|---|
STORAGE_DIR | ./veto-storage | SQLite + NDJSON lives here |
KEYS_DIR | ./veto-keys | PKCS8 PEM + JWKS JSON |
VETO_SIGNING_KID | default | Active signing kid |
MEOW_GATEWAY_PORT | 3005 | HTTP listener port |
LISTEN_HOST | 127.0.0.1 | Bind to 0.0.0.0 for containerized deploys |
MEOW_MODE | mock | live once you've completed Meow OAuth |
VETO_CAPSULE_MAX_TTL_SECONDS | 86400 | Hard 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_LEVEL | info | debug shows per-rule policy eval |
Health + readiness
GET /health— liveness probe; 200 iff process responsiveGET /ready— readiness probe; 503 if storage or signing unavailableGET /.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_hashissha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855(SHA-256 of the empty byte string) - Merkle root is recomputed every 1024 receipts — use
/v1/receipts/:entity/verifyto recheck the full chain