History and AS OF
With history on, every change is logged in the same transaction as the write: the new version of the record, who changed it, when, and why.
Turn it on
Enable it in the admin UI or with "history": true in the schema. Existing rows are logged once as a baseline, so the history starts complete.
Read the past
// the collection as it stood at the end of June
await sluurp.collection("grades").list({ asOf: "2026-06-30" });
// one record's versions, newest first, and restoring one
await sluurp.collection("grades").history(id);
await sluurp.collection("grades").restore(id, seq, { reason: "Undo" });GET /api/collections/grades/aggregate?op=avg&field=value&group=subject&asOf=2026-06-30
// every change, in order
GET /api/collections/grades/changes?since=0Each past version is checked against the collection’s view rule as it applied to that version, so history never shows anything the reader couldn’t have seen.
A past snapshot is rebuilt once and cached, so paging through it or querying it again is fast. The server caches a few snapshots per collection. If too many new snapshots are requested at once, the extra requests get 429 Too Many Requests and can retry a second later, so time travel never slows down everyone else.
Reasons
await sluurp.collection("grades").update(id, { value: 7 }, { reason: "Re-marked after appeal" });Over HTTP, use the X-Sluurp-Reason header. A batch applies one reason to all its changes.
In SQL
The admin UI’s SQL console supports SQL:2011 temporal syntax. Plain queries still read the latest rows.
SELECT subject, avg(value)
FROM grades FOR SYSTEM_TIME AS OF '2026-06-30'
GROUP BY subject;Before the query reaches SQLite, each such table is swapped for its snapshot, rebuilt from the change log under the same name. This also works in the browser on rows synced into SQLite.
Pages
Pages’ version history uses the same log: who changed a page, when, and why. Restoring a page restores one of its versions.