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.
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.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.