Persist and sync OpenUI generated interface state across reloads and clients using a single Convex table with debounced writes and conflict resolution.
npm install openui-convexopenui-convex adds durable, reactive per-message state to OpenUI generated interfaces running on Convex. It stores form and widget state in a single table keyed by (scopeKey, messageId), with last-write-wins versioning, so values survive page reloads and stay in sync across browser tabs. The package provides server-side helpers, a React hook that debounces writes and defers remote updates while the interface has focus, and a JSON encoding layer that handles the $binding keys OpenUI uses internally.
The openui-convex package provides a useOpenUIState React hook that loads a saved snapshot from Convex before mounting the Renderer, so the form hydrates from persisted state instead of defaults. It debounces writes at 400ms by default and encodes the state as a JSON envelope to handle OpenUI's $binding keys, which Convex would otherwise reject as invalid object keys.
openui-convex stores state as a single record keyed by (scopeKey, messageId) with a version counter. The useOpenUIState hook subscribes each interface to its own record and defers applying remote snapshots while the user has focus or a write is in flight, then applies them after. This keeps two open windows consistent without clobbering in-progress edits.
When using Convex Agent for streaming, pass the thread ID as scopeKey and the message key as messageId to useOpenUIState. The component stores one snapshot per assistant message and guards access through your own checkRead and checkWrite authorization hooks, so thread-level access control also applies to interface state.
The openui-convex server helper clearState deletes one interface or an entire scope, processing at most 100 records per call and returning hasMore when more remain. You can use ctx.scheduler.runAfter(0) inside an internalMutation to chain calls until the scope is fully cleared without hitting transaction size limits.
Authorization is your responsibility. The OpenUI Convex component cannot access ctx.auth directly, so you must wrap its API by instantiating OpenUI and passing checkRead and checkWrite hooks that enforce your own access rules. You then export the resulting getState, setState, and clearState functions from your convex/ directory and expose only those to the browser.
openui-convex attempts a best-effort flush on unmount and on the pagehide event, but a closed tab or lost connection can lose the last debounce window of edits. The default debounce is 400ms, and you can call the flush() method returned by useOpenUIState before app-controlled navigation to guarantee pending writes are committed.
openui-convex uses last-write-wins with a version counter. Every write increments the version, and the hook ignores any incoming snapshot older than the version it already saved. There is no merge strategy because the component is designed for form state, not collaborative text editing.
Each snapshot is limited to 64 KiB of JSON. Writes exceeding that limit or containing malformed data are rejected before reaching storage. The state is stored as a JSON envelope with the key openuiEncoding set to json-v1, which the hook and server helpers encode and decode automatically.
openui-convex has no runtime dependencies. The convex package is a peer dependency, and react is an optional peer dependency used only by the openui-convex/react entry point. You import the React hook separately from the server helpers, so server-only code does not pull in React.