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 off, the table isn't created, nothing is
captured, and the built-ins aren't served or codegen'd.
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).