@checkfu/ui is a React library your application installs and renders itself. There is no iframe, hosted widget, or script tag. The components take event data and emit intents; they know nothing about your router, state library, authentication, or backend framework.
Install and render
React 19 andreact-dom are the only peer dependencies.
Private alpha: the package is not yet published to npm. Today it lives inside the platform repository while the public release is prepared, so the install below does not resolve yet. The component surface is the stable one.
--cfui-* custom properties are the entire theme boundary; no Tailwind setup is required.
EventRow, ToolCallCard, SessionStatusBadge, and the compound ActionApprovalPrompt. Each has its own subpath for hosts that want one piece rather than the barrel.
Give the browser one Session, not your Workspace key
Keep the Checkfu API key on your server. After authenticating your end user and resolving their person Principal, mint a short-lived capability for the exact Session and actions the UI needs:SessionClient has no generic request escape hatch and accepts no API key or Workspace selector. It can list/stream events and send only messages, interrupts, or Question answers allowed by the token. ActionApproval remains a server-owned flow.
For a trusted embedded agent surface that also needs the active managed agent’s
released Connections, mint invoke_tools in addition to the event permissions.
This permission is available only while an installed Session has an active Run:
invoke_tools is consequential:
give it only to the intended trusted origin and request it only when the UI
actually needs tool access.
The stream is data, not a change bell
useSessionEvents from @checkfu/ui/sdk owns the whole loop. Hand it your client — a SessionClient for browser-direct mode, or the Workspace-proxy transport when your backend’s two forwarding routes hold the key:
seq (appending event N never re-reads 1..N-1), streaming previews render as ephemeral deltas and are replaced by their durable event, and a checkfu.cursor_gap triggers exactly one bounded backfill page for the gap range. Reconnects resume losslessly from the last durable seq, with a bounded five-second poll only while the stream is down.
Honesty is part of the result: state.unavailable carries the durable positions whose content the platform cannot produce (crypto-shredded, zero data retention, or an unaccounted gap), and TranscriptView renders them as explicit rows — a complete transcript and a shredded one never look the same. In transcript mode, keyboard reading order visits each unavailable range as one disabled option; it is announced without changing the event-only selectedSeq. state.previews carries in-flight streaming deltas; render them with StreamingDeltaRow, which is explicit about being ephemeral. The rest of the chat surface is composable the same way: Composer (drives user.message with disabled-with-reason states), QuestionCard (agent.question → user.question_answer), InterruptButton (cooperative drain vs immediate force cancel), and SessionList — all intent-emitting, none fetching. In QuestionCard, empty options render one bounded free-text answer, and a new Question id clears every local draft before it renders.
The SDK’s stream is the only SSE reader involved — no host application parses data:/id: lines. See Resume a stream reliably for the cursor rules the SDK implements.
ActionApproval intents
The compound approval component keeps API ownership in your host:onIntent receives one of three UI intents: approve, deny, or instructions. The first two map to the ActionApproval API’s decision enum — it accepts approve | deny and nothing else. instructions is not a decision: it is the reviewer asking to attach a note, which your host renders and then sends as the optional instructions string alongside an approve or deny. Route the resulting decision to your authenticated backend, which uses the TypeScript SDK to respond to the durable ActionApproval. The component never responds on its own.