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

Wire 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 null

Try 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 — wire rowHistory / rowAsOf into 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.