Time-travel debugger

Record the app's ChangeEvent timeline and scrub a table's rows back and forward through it — read-only point-in-time replay, env-gated + redacted, surfaced in the DevTools / cloud Time-travel panel.

The time-travel debugger records the app's ChangeEvent stream into a bounded, redacted ring and lets you scrub any table's rows back and forward through it — replaying what the data looked like at any recorded point. Read-only: replay reconstructs a row-set by reversing the recorded changes over the live rows; it never writes.

Turn it on

Recording is env-gated and off by default in production (volume + PII are the real cost — use OTLP for prod forensics). Set VOLTRO_TIMELINE:

value records
off (also 0 / false) nothing
interesting (also on / 1 / true) user tables only (skips _voltro_* framework noise)
all every table (except the timeline's own writes)

The default is interesting outside production, off in production — the same environment-aware default the durable trace persistence uses.

Sensitive-looking columns (password / secret / token / api_key / authorization / cookie / ssn / credit_card / cvv / …) are redacted before a row enters the ring — PII never sits in the debug buffer.

Scrub it

Open the Time-travel panel in the DevTools dashboard (or the cloud dashboard, for a deployed app): pick a table, drag the scrubber across the recorded sequence, and the panel reconstructs that table's rows as of the selected point. The event log shows every recorded change; click one to scrub to just before it. "Jump to now" returns to live.

Inspect endpoints

Both endpoints use the app's inspect authentication and are available in development, source serve and compiled production builds. The example uses curl and jq; supply your configured inspect authentication when required.

  • GET /_voltro/inspect/timeline returns recording state, recorderId, currentSeq and events. Optional filters are table, tenantId, from, to and limit.
  • GET /_voltro/inspect/timeline/replay?table=<t>&seq=<n>&recorderId=<id> reconstructs the selected table within that recorder's retained history.
# Use the identity and sequence from the same successful timeline response.
timeline_snapshot=$(curl --fail --silent "http://localhost:4000/_voltro/inspect/timeline?table=todos&limit=50")
timeline_recorder=$(printf '%s' "$timeline_snapshot" | jq -r '.recorderId')
timeline_sequence=$(printf '%s' "$timeline_snapshot" | jq -r '.currentSeq')
curl --fail --get "http://localhost:4000/_voltro/inspect/timeline/replay" --data-urlencode "table=todos" --data-urlencode "seq=$timeline_sequence" --data-urlencode "recorderId=$timeline_recorder"

A missing or replaced recorder identity, an expired position or a change during the replay read returns HTTP 409. Reload the timeline and explicitly select a current position; never reuse an old sequence with a new identity. The dashboard's Jump to now action does this.

Limits

  • Replay is read-only. It never restores or changes application rows.
  • History is bounded and process-local. Restarting or invalidating the recorder retires its identity. Choose one replica for historical replay; a sequence from one process is not a position in another process.
  • Missing, oversized or coalesced change images cannot reconstruct an exact earlier state; replay rejects history crossing those events.
  • Production masks .sensitive(), .encrypted() and .serverOnly() fields before recording and again when returning events or replay rows. Sensitive-looking field names are also redacted. Native SQLite, native local Turso and PostgreSQL apply the same schema masking in development because their source journals persist row images. Other development stores keep their existing local schema visibility.
  • For stores other than native SQLite, native local Turso and PostgreSQL, replay reads all table pages in one store transaction, using the dialect's existing isolation. This does not establish atomicity between database rows and external change delivery. Writes absent from the process change stream remain an open consistency limitation; Timeline is not a complete database audit log. Production recording stays off by default.

Migration tombstones declared with dropped() are not captured columns. They are excluded from journal images and current-row snapshots, including while a former physical column still exists.

Native SQLite source capture

Replacing the capture schema or privacy contract retires existing recording leases and clears retained capture images in the same migration transaction. If that transaction rolls back, the previous capture state is preserved.

Capture checks the installed trigger definitions before accepting history. Catalog whitespace formatting is ignored; changes to quoted values, identifiers, operators or token boundaries still invalidate the contract.

The source position follows journal entries visible in the same transaction, together with the retention floor. A sequence number reserved by an unfinished or rolled-back write does not advance the reader or a newly activated observer.

Writes without an active recording lease do not retain row images. They change the source identity transactionally instead. If such a write commits after an observer activates, that observer refuses the incomplete history and starts a fresh baseline; a rolled-back write does not invalidate it. Existing capture triggers are refreshed through the normal schema apply/deploy path.

For native SQLite, development schema setup and successful voltro db apply, voltro db deploy, or voltro migrate --create-only prepare the capture schema after the application and runtime storage schemas. Serving does not install or repair capture schema. After a schema-changing step/file migration, snapshot restoration, or garbage collection, run the normal schema apply/deploy path before resuming historical inspection.

Enabled SQLite recording reads a bounded database change journal. Writes through another connection or replica are included while a recording lease is active. Table rows, source position and journal events are read in one transaction; paginated table reads use that same snapshot. Sensitive, encrypted and server-only fields are masked before persistence, including in development. Unsupported or oversized images make that history unavailable rather than guessing earlier values.

The server renews its lease in the background and releases it on shutdown. Expiration, schema incompatibility or a retention gap retires the recorder identity; recovery starts at the current source position and never bridges an unrecorded interval. An unprepared database reports unavailable history instead of silently falling back to process-local changes. This remains a bounded debugger, not an audit log. Stores without source capture retain their existing change-stream consistency limits.

PostgreSQL source capture

PostgreSQL uses a transactional source journal in the connected database namespace (current_schema()). Development schema setup and successful voltro db apply, voltro db deploy, or voltro migrate --create-only prepare its tables and triggers after application and runtime storage. Source and compiled production serving only read this contract; they do not install or repair it. An unprepared or incompatible source reports unavailable history.

While a recording lease is active, committed writes from other connections and replicas are captured even if LISTEN change notifications are interrupted. Each read uses one repeatable-read snapshot for the source events and all requested table pages. Transaction visibility determines which changes are new: an allocated journal number is not commit order, so a transaction that commits late is not skipped. Sensitive, encrypted and server-only fields are masked before journal persistence, including in development. Unsupported or oversized images make history unavailable instead of allowing an approximate replay.

Each recording process renews a database-time lease in the background and releases it on graceful shutdown. Expired leases, incompatible schemas and retention gaps retire the recorder identity. Collection invalidates readers that still need discarded events; recovery starts from a fresh snapshot without bridging missing history. When a writer sees no active lease, it stops saving row images and records a compact transaction gap instead. An abandoned lease can retain images until it expires. This is bounded debugging history, not a durable audit log or cross-replica sequence.

After a schema-changing file or step migration, snapshot restoration or schema garbage collection, use the normal apply/deploy path to prepare capture again. Schema maintenance retires the old contract before changing tables, so historical inspection cannot silently reuse it.

Native local Turso source capture

Native local Turso uses a fixed ring of 5,000 capture slots. Development startup and successful voltro db apply, voltro db deploy, or voltro migrate --create-only prepare the capture contract. Source and compiled production serving validate and read it without installing or repairing it.

Readers track each slot's generation in a database snapshot, together with all requested row pages. A lower sequence that commits late is still collected. Overwritten slots, writes outside an active recording lease, an expired lease, or an incompatible contract invalidate history; recovery starts a new baseline. The ring is bounded storage, not a guarantee of the last 5,000 committed changes or an audit log.

Sensitive, encrypted and server-only fields are masked before capture, including in development. The undo ledger's serialized row images are server-only too, so nested application values cannot bypass this masking through that ledger. The undo engine retains its original data and behavior.

This applies to the native, single-process embedded Turso backend. Remote libSQL and embedded-sync clients do not use this source capture. It does not provide concurrent cross-process access to one native database file.