Skip to content

Record history

Vaultbase records every write to a history-enabled collection in an append-only vaultbase_record_history table. Each row captures the record’s post-write state (or pre-delete state on delete) plus the actor that triggered the change.

Set history_enabled: true when creating or updating the collection:

PATCH /api/v1/collections/<id>
{ "history_enabled": true }

…or check the History toggle in the admin schema editor.

When the flag is off, the records API skips the history write entirely — no storage cost, no overhead. Flipping it on starts capturing immediately but does not backfill prior writes.

CREATE TABLE vaultbase_record_history (
id TEXT PRIMARY KEY,
collection TEXT NOT NULL,
record_id TEXT NOT NULL,
op TEXT NOT NULL, -- "create" | "update" | "delete"
snapshot TEXT NOT NULL, -- JSON-encoded record state
actor_id TEXT, -- caller id, or NULL
actor_type TEXT, -- "user" | "admin" | NULL
at INTEGER NOT NULL -- unix seconds
);

snapshot is the post-write record on create / update and the pre-delete record on delete.

GET /api/<col>/<id>/history?page=1&perPage=50
Authorization: Bearer <token>

Gated by the parent record’s view_rule — if you can view the live record, you can read its history. Pages are ordered newest-first.

POST /api/<col>/<id>/restore?at=<unix-seconds>
Authorization: Bearer <admin-token>

Admin-only. Restores the record to the most recent snapshot at or before the given timestamp. The live record’s updated_at advances and the restore is itself logged as a fresh update row in history.

Restoring a deleted record returns 409 in v1 — createRecord mints its own id, so re-creating with the original id is unsupported. Workaround: POST /api/<col> with the snapshot body (you’ll get a new id).

History rows accumulate forever by default. Drop ancient entries via a cron job:

// cron: 0 3 * * *
const days = 90;
const cutoff = Math.floor(Date.now() / 1000) - days * 24 * 3600;
const removed = await ctx.helpers.db.exec(
"DELETE FROM vaultbase_record_history WHERE at < ?", cutoff,
);
ctx.helpers.log(`pruned ${removed.changes} history rows older than ${days}d`);
  • File contents. Only the filename string in the snapshot. Restoring brings back the field value but the underlying file must still exist on disk / S3.
  • Schema changes. This is a record-level log; collection schema diffs live in the migrations subsystem.
  • Reads. Only writes are persisted.

Each enabled write does one extra INSERT (parameterised, transactional) plus serialising the snapshot to JSON. Indexes cover (collection, record_id, at) and (at) for prune sweeps, so the read endpoints are O(log n).