Record history
Per-collection append-only audit log of record writes, with point-in-time restore.
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.
Enabling history on a collection
Section titled “Enabling history on a collection”New collections keep history unless you turn it off (since 0.40.0; before, it was off unless turned on). That holds over the API, MCP and the admin. Views never keep history. Collections made before 0.40.0 keep the setting they had.
Turn it on or off when creating or updating the collection:
PATCH /api/v1/collections/<id>{ "history_enabled": true }…or use the Keep record 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.
Storage shape
Section titled “Storage shape”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.
Fields marked encrypted in the collection’s schema are decrypted when
history is read, as on record reads. Other fields come back as stored, even
if their value looks like ciphertext.
Deleting a collection deletes its history, so a new collection with the same name starts with none.
Browse
Section titled “Browse”GET /api/v1/<col>/<id>/history?page=1&perPage=50Authorization: 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. For a non-admin, a
collection whose list, view and create rules are all admin-only answers 404
here, as a missing one does.
Admins see every version. Anyone else sees a version only if the view rule
passes against that version as it stood, so a record that changed hands
doesn’t show the new owner what the previous one wrote; with a view rule,
the newest 2,000 versions are checked. With no view rule every version is
listed and counted (since 0.37.0; before, at most 2,000). actor_id is
shown only on the caller’s own changes.
Restore
Section titled “Restore”POST /api/v1/<col>/<id>/restore?entry=<history-id>POST /api/v1/<col>/<id>/restore?at=<unix-seconds>Authorization: Bearer <admin-token>Admin-only. entry restores exactly that version (the id of a history
row). at restores the most recent snapshot at or before that second;
several writes in one second share an at, so at takes the newest of
them. The live record’s updated_at advances and the restore is itself
logged as a fresh update row in history, and in the
audit log as record.restore.
Undo a delete
Section titled “Undo a delete”When the record’s latest change is a delete, a restore brings it back under
the same id, with the values of the version you pick (the delete row holds
the record as it was just before the delete) and its original creation time.
The history gets a create row. In the admin, open the record’s history
(from its audit log entry, for instance) and click
Undo delete.
- Base collections only. A deleted auth user or a singleton answers
409. - Records a cascade deleted with it stay deleted; restore each one from its own history.
- A relation to a record that no longer exists fails validation (
422).
Retention
Section titled “Retention”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.sql.exec( "DELETE FROM vaultbase_record_history WHERE at < ?", [cutoff],);ctx.helpers.log(`pruned ${removed.changes} history rows older than ${days}d`);What history does not track
Section titled “What history does not track”- 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.
Performance
Section titled “Performance”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).