Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

OpenUI Generative UI

Durable, reactive state for OpenUI generated interfaces on Convex. Forms keep their values across reloads and stay in sync across clients.

Upstream source
git clone https://github.com/aayush962/openui-convex.git
cd openui-convex

This upstream package has not been adapted or qualified for Bijection. Read the original documentation before adopting it.

Description

Durable, reactive state for OpenUI generated interfaces on Convex. Forms keep their values across reloads and stay in sync across clients.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: aayush962/openui-convex. License: See repository. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

Durable, reactive OpenUI interface state on Convex.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

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

Use cases

From the upstream package documentation.

  • Persisting form field values in AI-generated UIs so users do not lose their input on page reload or navigation
  • Syncing interactive widget state across multiple browser tabs or clients connected to the same Convex deployment
  • Integrating per-message UI snapshots into a Convex Agent chat thread, scoped by thread ID so authorization carries over
  • Cleaning up all interface state for a deleted thread or document using clearState with paginated batch deletes
  • Testing Convex functions that depend on component state without a live deployment, using the convex-test register helper

How it works

From the upstream package documentation.

Mount the component in convex/convex.config.ts using app.use(openui), then create a wrapper module in your convex/ directory that instantiates OpenUI and exports getState, setState, and clearState with your own checkRead and checkWrite authorization hooks. The component cannot access ctx.auth directly, so your hooks enforce access control per scopeKey before any read or write reaches storage. scopeKey and messageId are opaque strings you supply, such as a thread ID paired with a message ID.

On the client, useOpenUIState from openui-convex/react subscribes to the stored snapshot for a given (scopeKey, messageId) pair and returns initialState, a stable key, onStateUpdate, containerProps, and a flush function. Pass initialState and key to your Renderer so it hydrates from the saved snapshot rather than its defaults. onStateUpdate is undefined while streaming or loading, which prevents partial state from being written during generation. Writes are debounced at 400ms by default and flushed on unmount and pagehide.

State is stored as a JSON envelope using the openuiEncoding: json-v1 format because Convex rejects $binding as an object key. The hook and server helpers encode and decode transparently. Snapshots are capped at 64 KiB; larger or malformed payloads are rejected before storage. Remote updates are deferred while the container has focus or a write is in flight, then applied once the user tabs away, avoiding overwrites of in-progress edits.

For cleanup, clearState deletes up to 100 records per call and returns hasMore. Chain it through the Convex scheduler using ctx.scheduler.runAfter to handle scopes with many records across multiple transactions without hitting mutation time limits.

Upstream package FAQ

Does openui-convex handle authorization, or is that my responsibility?

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.

What happens to state if the user closes the tab before the debounce window completes?

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.

How does openui-convex handle conflicts when two clients write at the same time?

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.

What are the storage limits for a single generated interface snapshot?

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.

Does openui-convex add any runtime dependencies to my project?

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.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate OpenUI Generative UI with Bijection.

Read: https://bijection.com/components/openui-convex/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from aayush962/openui-convex, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

openui-convex

Durable, reactive state for OpenUI interfaces on Convex.

A generated form or interactive interface keeps its state across reloads and stays in sync across clients. Your application keeps control of rendering, actions, tools, and AI generation.

  • One table: (scopeKey, messageId) → state, versioned, last write wins.
  • Server helpers and browser wrappers that require your own authorization hooks.
  • A React hook that hydrates your Renderer, debounces and orders writes, and applies remote updates without clobbering what the user is editing.
  • No runtime dependencies. convex is a peer; react is an optional peer used only by openui-convex/react.

Status: 0.1.0, an early release. The API may change before 1.0.

Install

npm install openui-convex

Mount the component in convex/convex.config.ts:

import { defineApp } from "convex/server";
import openui from "openui-convex/convex.config.js";

const app = defineApp();
app.use(openui);
export default app;

Expose it behind your authorization

A component cannot see ctx.auth, so both hooks are required. Export the wrappers from a module in your convex/ directory, for example convex/openui.ts:

import { OpenUI } from "openui-convex";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { requireScopeAccess } from "./auth"; // your check: throws unless the caller may use this scope

export const openui = new OpenUI(components.openui);
export const { getState, setState, clearState } = openui.api<DataModel>({
  checkRead: requireScopeAccess,
  checkWrite: requireScopeAccess,
});

Export only the wrappers browsers should call. scopeKey and messageId are opaque strings you choose: a thread id and message id, a document id and block id, a dashboard id and widget id.

Render with your own Renderer

import { Renderer } from "@openuidev/react-lang";
import { useOpenUIState } from "openui-convex/react";
import { api } from "../convex/_generated/api";

function GeneratedUI({ threadId, message }: { threadId: string; message: { key: string; text: string; status: string } }) {
  const streaming = message.status === "streaming";
  const ui = useOpenUIState({ api: api.openui, scopeKey: threadId, messageId: message.key, isStreaming: streaming });
  if (ui.isLoading) return null;
  return (
    <div {...ui.containerProps}>
      <Renderer
        key={ui.key}
        library={library}
        response={message.text}
        isStreaming={streaming}
        initialState={ui.initialState}
        onStateUpdate={ui.onStateUpdate}
        onAction={handleAction}
      />
    </div>
  );
}

What the hook returns:

  • isLoading: wait for it before mounting, so the Renderer hydrates from the saved snapshot instead of its defaults.
  • key and initialState: pass both. The key changes when a remote snapshot replaces local state, and the remount clears fields the snapshot no longer contains.
  • onStateUpdate: undefined while streaming or loading, so incomplete state is never saved.
  • containerProps: spread onto the element around the Renderer. Remote updates wait until focus leaves it.
  • flush(): writes pending edits now. Await it before app-controlled navigation.

Options: debounceMs (default 400) and onError, which receives failed writes and invalid values.

Actions, tool providers, and the component library stay yours. The hook never sends messages or opens links.

Contract

  • Conflicts. Last write wins and every write increments version. The hook ignores snapshots older than what it already saved, defers remote snapshots while a write is in flight or the interface has focus, and applies them afterwards. There is no merge: this is form state, not collaborative text.
  • Encoding. OpenUI state uses $binding keys, which Convex rejects as object keys. On the wire and in storage the state is a JSON envelope, { openuiEncoding: "json-v1", data: "..." }. The hook and the server helpers encode and decode for you; use encodeState and decodeState if you call the wrappers directly. Serialization follows JSON.stringify: undefined and functions are dropped, non-finite numbers become null, dates become ISO strings, and cycles or BigInt are rejected.
  • Limits. One snapshot is at most 64 KiB of JSON. Larger or malformed writes are rejected before storage.
  • Reads. Each interface subscribes to its own record. Nothing reads a whole scope, so a long thread costs only what is on screen.
  • Delivery. Unmount and pagehide flush pending edits on a best-effort basis. A closed tab or a lost connection can lose the last debounce window.
  • Cleanup. clearState deletes one interface or a whole scope, at most 100 records per call, and reports hasMore. Continue in a fresh transaction:
export const purgeThreadState = internalMutation({
  args: { threadId: v.string() },
  handler: async (ctx, { threadId }) => {
    const { hasMore } = await openui.clearState(ctx, { scopeKey: threadId });
    if (hasMore) await ctx.scheduler.runAfter(0, internal.threads.purgeThreadState, { threadId });
  },
});

Server helpers for your own functions: openui.getState (returns decoded state; encode it again before returning it to a client), openui.setState, and openui.clearState.

With Convex Agent

Convex Agent owns threads, messages, and streaming. This component adds persistent state per assistant message; use the thread id as the scope so thread authorization also guards interface state.

const { results } = useUIMessages(api.chat.listMessages, { threadId }, { initialNumItems: 20, stream: true });
// For each assistant message:
const ui = useOpenUIState({
  api: api.openui,
  scopeKey: threadId,
  messageId: message.key,
  isStreaming: message.status === "streaming",
});

The example/ app is a complete version of this recipe: example/convex/chat.ts streams OpenUI Lang through Agent, example/convex/openui.ts exports the wrappers, and example/src/pages/Chat.tsx renders each message.

Testing

openui-convex/test registers the component with convex-test:

import { convexTest } from "convex-test";
import { register } from "openui-convex/test";
import schema from "./schema";

const t = convexTest(schema, import.meta.glob("./**/*.ts"));
register(t); // mounts the component as "openui"

Run the example

Requires Node 20.19+ or 22.12+ and pnpm 10.7.1.

pnpm install --frozen-lockfile
npm run dev            # Convex dev deployment plus package codegen and build
npm run dev:frontend   # Vite on http://127.0.0.1:5173, in a second terminal

Configure the model on your development deployment:

npx convex env set ANTHROPIC_API_KEY <your-key>
npx convex env set OPENUI_TELEMETRY_DISABLED 1
npx convex env set ANTHROPIC_MODEL claude-sonnet-4-6   # optional

Ask for a form, edit it, reload the page or open a second window, and the values are still there.

The example is a shared, unauthenticated sandbox: anyone who reaches its backend can read threads and spend model credits, and nothing serializes concurrent generations in one thread. Add identity, ownership checks, and rate limiting before hosting it publicly, and keep model keys out of VITE_ variables.

Development

npm run check   # build, typecheck, lint, tests, example build, export check

Checks run against the committed generated files and need no deployment. After changing the component's functions, run npm run build:codegen against a development deployment to regenerate src/component/_generated. example/convex/openui.test.ts shows how an app tests code that uses the component. Releases are described in PUBLISHING.md.

Keywords