Time Travel

Synced Evolu tables mark rows as deleted and retain their history so devices can merge changes after reconnecting. Local tables, whose names start with _, delete rows permanently and do not keep sync history.

Imagine this scenario: you delete a piece of data on a disconnected device, while another device updates that same data. Should the update be discarded? To enforce true deletion across all devices—even future ones—would require complex logic to reject the data forever, without exposing the original data (for security reasons). This is possible (and planned for Evolu), but it's not trivial.

For synced tables, Evolu stores column changes—including the isDeleted flag—in evolu_history. This history enables time travel.

Using the Schema, TodoId, and Evolu instance from Get started, query the history of a specific todo's title:

import {
  createQueryBuilder,
  idToIdBytes,
  millisToDateIso,
  timestampBytesToTimestamp,
} from "@evolu/common";

const createQuery = createQueryBuilder(Schema);
const titleHistoryQuery = (todoId: TodoId) =>
  createQuery((db) =>
    db
      .selectFrom("evolu_history")
      .select(["value", "timestamp"])
      .where("table", "=", "todo")
      .where("id", "=", idToIdBytes(todoId))
      .where("column", "=", "title")
      // Narrows the TypeScript type; it does not validate stored values.
      .$narrowType<{
        value: (typeof Schema)["todo"]["title"]["Output"] | null;
      }>()
      .orderBy("timestamp", "desc"),
  );

const handleHistoryClick = (todoId: TodoId) => {
  void evolu.loadQuery(titleHistoryQuery(todoId)).then((rows) => {
    const rowsWithTimestamp = rows.map((row) => ({
      value: row.value,
      timestamp: millisToDateIso(
        timestampBytesToTimestamp(row.timestamp).millis,
      ),
    }));
    alert(JSON.stringify(rowsWithTimestamp, null, 2));
  });
};

This API isn’t fully type-safe, but it’s not a concern. Evolu Schemas are append-only. Once an app is released, do not rename or change the type of an existing table or column — only add new tables or columns to evolve your schema; changing existing columns or types breaks compatibility with historical data.