History

Learn how to query and subscribe to earlier versions of your data.

Every table has a read-only history table, available as app.$history.tableName. It contains all accepted versions of each row, including the current version. Pending writes do not appear until the server accepts them.

Querying history

Filter by a row ID to read its versions:

const versions = await db.all(
app.$history.tasks
.where({ id: taskId })
.orderBy("$acceptedAt", "desc")
.select("title", "$acceptedAt", "$transactionId"),
);

Every version keeps the original row's id. Use id and $transactionId together to distinguish versions; id alone is not unique in history results.

You can filter historical column values, select fields, sort, and paginate. To read several rows' histories, use an explicit ID list:

const versions = await db.all(
app.$history.tasks
.where({ id: { in: taskIds }, done: false })
.orderBy("$acceptedAt", "desc")
.limit(20),
);

You can also subscribe to historical data. New accepted versions will update the result.

Version metadata

History rows include two additional columns by default:

ColumnTypeScript typeMeaning
$acceptedAtDateWhen the server accepted the version.
$transactionIdTxIdThe transaction that wrote the version.

The usual attribution columns ($createdAt, $createdBy, $updatedAt, and $updatedBy) are also available. Anyone who can read a row's history sees the author of every version, including identity.subject; see Metadata.

Local and remote reads

History uses the same read tiers as ordinary queries:

  • "local-first" reads locally available history and fetches missing history from upstream. A subscription updates as that history arrives. The initial result may be incomplete; use firstLoadRemoteWaitMs to allow a bounded wait.
  • "remote" waits for the server's result, fetching the required versions.
const versions = await db.all(history, { tier: "remote" });

Neither tier exposes pending versions in history.

Permissions

Access to history follows the current row's read policy. If you can read the current row, you can read its previous values, even if those values would not satisfy the policy themselves.

Deleted rows

Deleting the current row removes its history from the default result; restoring it makes the retained versions visible again.

Use .includeDeleted() to read previous versions of a deleted row:

const versions = await db.all(history.includeDeleted());

Deletion-only and restore-only events do not add content versions. Restoring with replacement content does.

Branches

For a branched table, select one branch through query options:

const versions = await db.all(history, { branch: "draft", tier: "remote" });

Only that branch's versions are returned. Server permissions use the current row in that branch. History queries do not yet support an inherited base.

This makes private drafts possible. Write a draft's edits to a branch that only its author can read, then merge by writing the final content into main. The merge lands in main as one new version, so readers of main see it without the draft's intermediate versions, and readers who can't read the draft branch get no draft history at all.

Schema changes

History is read through schema lenses using the querying app's schema. Renamed columns use their new names, added columns receive their lens defaults, and removed columns are omitted.

Limitations

  • Every history source must be constrained by an exact id or an explicit id.in list. Queries over all rows' histories are unsupported.
  • $transactionId can be selected but cannot be used in filters.
  • History is indexed by row ID only. Additional filters are evaluated over those rows' versions.
  • History tables cannot be mutation targets or used on either side of .include(), .hopTo(), or .gather().
  • History queries do not yet support using base branches.
  • Tables declared latest-only keep no history and refuse history queries.