useUndoLog

Universal end-user undo + redo over mutations, server-persisted via synthesized inverses.

End-user Ctrl-Z over mutations, server-backed. When undo capture is on, the framework records each mutation's change-set (the CDC {old,new} per row) into _voltro_undo_log — inside the mutation's own transaction, so a rolled-back write leaves no entry. Three built-in rpc procedures expose it, typed end-to-end via codegen:

  • __voltro.undo.log — a reactive, subject-scoped feed of your recent undoable actions (newest first).
  • __voltro.undo.apply / __voltro.undo.redo — undo / redo one invocation by id.

useUndoLog(api) is the client controller:

const undo = useUndoLog('app')
// → { entries, loading, canUndo, canRedo, undo, redo, undoLast, redoLast }

<button disabled={!undo.canUndo} onClick={() => undo.undoLast()}>Undo</button>
<button disabled={!undo.canRedo} onClick={() => undo.redoLast()}>Redo</button>

Undo applies the synthesized inverse as a normal store write — it traverses the same tenant/RBAC guards, is reactive (the affected rows reappear / vanish live, even across tabs), and is audited. It is per-actor (you can't undo another subject's action), refuses on a concurrent-change conflict or past an action boundary (an external side effect), and is idempotent. Because the stack lives on the server, it survives a page reload.

Enabling — VOLTRO_UNDO

Capture has a per-mutation cost (a read-before-write on update/delete + a log-row insert), so it's on outside production, off in production by default; set VOLTRO_UNDO=on|off to override (the same environment-aware default the durable trace persistence uses). When capture is off, _voltro_undo_log isn't created and nothing is recorded.

The WIRE SURFACE is a separate question, and it reads only the explicit declaration. The three __voltro.undo.* procedures are generated into the client's rpc group and bound on the server unless VOLTRO_UNDO=off — regardless of NODE_ENV. The reason is that rpcGroup.generated.ts is written by voltro dev and voltro build never regenerates it: an environment-derived answer baked there is the developer machine's answer shipped to a production process that binds none of it, and the failure appears only when someone presses undo after a deploy.

So with capture off, the procedures exist and answer honestly rather than 404: useUndoLog returns an empty list (nothing was captured) and apply/redo answer UndoNotFound. Both boot paths log a line at startup saying so, so a permanently empty undo list is not a mystery. VOLTRO_UNDO=off removes the procedures from the server AND from the client bundle.

Boundaries

Captures writes made through ctx.store (the same scope CDC covers) — raw unsafe() SQL writes and the bulk helpers (updateMany / deleteMany / upsert) aren't captured. Undo of an action-crossing invocation is refused (the engine's "can't undo past an external effect" wall).