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).