Skip to main content
Omit url for PGlite. dataDir defaults to .vendo/data. Set url for a hosted Postgres database. Both use the same schema and migration path. The stable public tables are vendo_meta, vendo_apps, vendo_records, vendo_blobs, vendo_state, vendo_threads, vendo_grants, vendo_approvals, vendo_audit, vendo_runs, and vendo_secrets. All structured payloads use jsonb. App record refs are GIN-indexed and can be joined to host tables with containment queries. Collection names use app:<appId>:<name>. App machines reach vendo_records and vendo_blobs through the guarded proxy, not directly. Each collection must appear in the app’s storage declaration with the refs keys it will write; the reserved name state is rejected. Records are capped at 256 KB, blobs at 5 MB, and every declared refs key must carry a non-empty string value at write time. See the app machine environment for the /data and /files proxy routes. encryption.key encrypts secret values in vendo_secrets only. App records stay queryable. Ephemeral principals use an in-memory overlay and never write to disk.

Encryption is on by default

vendo init provisions a 32-byte base64 key at VENDO_STORE_ENCRYPTION_KEY in your host .env. The default composition reads it when no store is passed to createVendo, so secret values in vendo_secrets are encrypted at rest without extra wiring. The key is append-safe: vendo init never regenerates it, even with --force. Do not rotate the key by hand — losing it makes existing secrets unreadable. An explicitly configured store always wins. If you pass store: createStore({ encryption: { key } }) to createVendo, the environment variable is ignored. Ciphertext is bound to the secret name with AES-GCM authenticated data, so a tampered or swapped row fails to decrypt. Rows written before this hardening keep decrypting and upgrade to the bound envelope on their next write.

Audit is append-only

vendo_audit is write-once. Calling put with an existing id fails with conflict, and delete is refused with blocked. The only sanctioned way to remove audit rows is the erase API below.

Ownership flips are refused

vendo_apps, vendo_grants, and vendo_threads all reject same-id writes that move a row to a different subject. If you need to transfer ownership, delete the row and re-seed it under the new subject.

Erasing data

eraseStore(store) is the sanctioned deletion path for vendo_audit and the one call that cascades across every store table plus the ephemeral overlay. Use it for right-to-erasure requests, tearing down a test app, or purging old runs.
Each call returns per-table deleted counts so you can log or verify the cascade. byAge treats a row as recent when any lifecycle timestamp (including updated_at) is at or after the cutoff — so a standing grant with a future expires_at and a rotated secret both survive an age sweep. Overlay blobs carry no timestamps and are exempt from the age axis; they end with the process.

Atomic claims

The store contract exposes a compare-and-set (CAS) claim primitive so concurrent workers can safely coordinate on a shared key without external locks. Use it whenever two ticks, retries, or agent runs might race on the same record — for example, deduplicating a webhook delivery, claiming a queued token exchange, or ensuring only one worker processes an invoice.
Claims are keyed per principal and expire after ttlMs, so a crashed worker never holds a key forever. acquired is false when another owner holds the key and its TTL has not elapsed. The same CAS machinery backs run-lifecycle transitions (start, stop, complete), so cancellation and scheduler ticks cannot resurrect a terminal run.