History
Every content write records a revision. The admin renders a field-level diff of any two consecutive ones and can put a single field back.
Plugin data
Section titled “Plugin data”Nothing, if its data goes through a form the engine already owns.
A plugin’s config settings, and the fields it contributes through a
page tab, are written by the engine’s own save path, so
they get revisions, diffs, the per-field clock and restore without a line of
code.
A plugin that writes its own table gets none of it. That is the strongest reason to express what you can as fields.
Revision rows
Section titled “Revision rows”interface Revision { id: number; batch: string; // one uuid per user action, however many rows it wrote scope: 'setting' | 'section' | 'page' | 'media' | 'people'; key: string; pageId: number; action: 'create' | 'update' | 'delete'; data: string | null; // the whole value after the write, as JSON position: number | null; enabled: boolean | null; source: 'admin' | 'ai' | 'import'; actor: string; // email createdAt: string;}data is the full value after the write, not a patch. Diffs are computed by
comparing a revision against the one before it, so a missing intermediate row
would produce a wrong diff rather than a gap. That is why the write matters more
than the read.
Authors
Section titled “Authors”import { authorOf } from '@ouncepage/core/guard';import { newBatch } from '@ouncepage/core/audit';
const author = authorOf(Astro.locals.editor, newBatch());// { email: 'someone@example.com', source: 'admin', batch: '...' }One newBatch() per user action, passed to every save it causes. Saving a page
and its five sections is one batch, so the activity view shows one entry that
expands into six, rather than six unrelated entries.
source is what separates a human’s change from a model’s. The history view
tags ai in amber.
Reading history
Section titled “Reading history”import { listRevisions, previousRevisions, revisionValue } from '@ouncepage/core/audit';import { diffForm } from '@ouncepage/core/diff';
const revisions = await listRevisions({ scope: 'setting', key: 'site', limit: 60 });const earlier = await previousRevisions(revisions);
for (const revision of revisions) { const before = revisionValue(earlier.get(revision.id) ?? null); const after = revisionValue(revision); const changes = diffForm(form, before, after);}diffForm walks the form shape rather than the raw object, so a change is
reported with the label an editor recognises rather than a dotted path. It
descends into group, indexes list entries and names each by its
titleField. Any scalar path the form does not cover is still compared, so a
schema that has drifted from its form shows up rather than disappearing.
Restore
Section titled “Restore”import { restoredValue } from '@ouncepage/core/restore';
const outcome = await restoredValue({ form, schema, current: await loadSetting('site'), source: revisionValue(previous), paths: ['site.brand'], // empty means every path in the diff});
if (outcome.ok) await saveSetting('site', outcome.value, author);Restore merges the chosen paths from the old value into the current one, then sanitises and validates the result. It is not a rollback: everything you did not select keeps its current value, and the restore itself is a new revision, so it is undoable in turn.
A restore that fails validation returns { ok: false, errors }. That happens
when a schema has changed since the revision was written, which is the correct
outcome: old content that no longer fits the current shape should not be written
back.
Per-field clocks
Section titled “Per-field clocks”Any field rendered with history on shows a clock that opens the dialog scoped
to that one path. It needs nothing from a field type: the engine places the
button beside your component in its own grid column.
media_uses records which content references which media URL. It is rebuilt in
full on every save rather than patched, which costs a full scan and cannot
drift. usesOf(url) answers “where is this used”, and pastUsesOf(url) scans
revisions for “where was it used”, which is what makes deleting an image safe to
reason about.