API · Versioning
Full row history + time-travel with @voltro/plugin-versioning — snapshots every insert/update/delete on the listed tables via the post-commit change tap; read every version with rowHistory() and the value at a past instant with rowAsOf(). audit() is who/when; this is what-changed-to-what. Memory store, zero infra.
Every version of a row, queryable. @voltro/plugin-versioning rides the post-commit ChangeEvent tap and records a FULL snapshot of every insert/update/delete on the listed tables. Read it back with rowHistory(store, table, id) (every version) and rowAsOf(store, table, id, when) (the value at a past instant — time-travel). audit() records WHO/WHEN; this records WHAT it changed to, so you can diff or restore. Memory store, zero infra. Template id: api-versioning.
Scaffold
voltro create-project acme --api=api-versioningWire it + read it back
// app.config.ts
import { versioningPlugin } from '@voltro/plugin-versioning'
export default { type: 'api', name: 'AcmeVer', store: 'memory',
plugins: [versioningPlugin({ tables: ['documents'] })] }The snapshotting is automatic. The point is to read history through handlers — the template ships two actions:
// documents.history.action.server.ts
import { rowHistory } from '@voltro/plugin-versioning'
export default async ({ id }, ctx) => {
const rows = await rowHistory(ctx.store, 'documents', id, ctx.request.subject.tenantId ?? null)
return rows.map((r) => ({ version: r.version, op: r.op, content: r.data?.content ?? null, changedAt: r.changedAt }))
}
// documents.asOf.action.server.ts
import { rowAsOf } from '@voltro/plugin-versioning'
export default async ({ id, at }, ctx) =>
rowAsOf(ctx.store, 'documents', id, ctx.request.subject.tenantId ?? null, at) // the row as it was at `at` (epoch ms), or nullTry it
ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d '{"tag":"documents.create","input":{"title":"Spec","content":"draft v1"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d "{\"tag\":\"documents.update\",\"input\":{\"id\":\"$ID\",\"content\":\"final v2\"}}" >/dev/null
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
-d "{\"tag\":\"documents.history\",\"input\":{\"id\":\"$ID\"}}"
# → [ { version:1, op:"insert", content:"draft v1", … }, { version:2, op:"update", content:"final v2", … } ]
# documents.asOf with v1's changedAt → { content:"draft v1" } (time-travel)audit vs versioning
| You want… | Use |
|---|---|
| WHO changed a row + WHEN | audit() mixin |
| WHAT it changed to (every prior value) | @voltro/plugin-versioning (this) |
They compose — audit() for accountability, versioning for diff/restore.
Production
The memory history store is single-process. With a SQL store, history persists in _voltro_row_history. High-churn tables grow history fast — version what you need to diff/restore, not everything.
Anti-patterns
- Listing
versioningPlugin({ tables })and stopping there. That records history but never shows it. The value is in the READS — wirerowHistory/rowAsOfinto a handler (or the dashboard) like this template does. - Versioning every table. Snapshots cost storage proportional to write volume. Pick the tables whose past values you actually need.