Option B: embed the chat component
The hosted chat page is built from one React component, <BuzzChat>, in the package @ltinteg/app-component-chat (version 0.9.0 at the time of writing). Embedding it gives your app the same chat, with every structured card, the card toolbar, row actions and the / palette, inside your own screens. Structured chat is part of DGI and is Alpha.
The actor side is configured exactly as in Option A, steps 1 to 6 (you can skip publishing the hosted page). This page covers the client.
Install
Add the package you received to your app, together with its peers:
| Peer | Version | Why |
|---|---|---|
react, react-dom | 19.2 or later | The component is React 19 |
@ltinteg/workflows-sdk | any | The client that starts the chat's entry points as the signed-in person. See Node SDK |
@orkestia/auth | any | Signs the person in with your identity app. See Auth SDK |
tailwindcss | 4.2 or later, optional | Only if you extend the component's Tailwind preset |
Import the stylesheet once:
import '@ltinteg/app-component-chat/style.css'
1. Sign the person in
Your end users sign in with your identity app, exactly as in Sign in with Orkestia. The session token is an ordinary end-user bearer token:
import { createOrkestiaAuth } from '@orkestia/auth'
import { LtIntegWorkflowsClient } from '@ltinteg/workflows-sdk'
const auth = createOrkestiaAuth({ clientKey: CLIENT_KEY })
// signIn() redirects to the hosted login; handleCallback() runs on your redirect page.
const session = auth.getSession() ?? await auth.handleCallback()
if (!session) await auth.signIn()
const client = new LtIntegWorkflowsClient({ baseUrl: API_BASE_URL, token: session.token })
CLIENT_KEY is your identity app's public client key. API_BASE_URL is the same workflow API address the SDKs use.
2. Bootstrap the chat
import { fetchChatBootstrap, openBuzzSession, RelayConnection } from '@ltinteg/app-component-chat'
// Public: the relay URL, the entry points your app exposes, the published theme, attachment limits.
const bootstrap = await fetchChatBootstrap(API_BASE_URL, CLIENT_KEY)
// As the signed-in person: starts the `open` entry point and receives their chat key once.
const { session: chat, signer } = await openBuzzSession({ client, bootstrap })
// The key lives only inside `signer`, in memory.
const connection = new RelayConnection({ relayUrl: chat.relayUrl, signer })
Nothing in the bootstrap is secret. The person's chat key never touches local storage, cookies or IndexedDB: a reload opens a new chat session.
3. Mount <BuzzChat>
import { useCallback, useMemo } from 'react'
import { BuzzChat, runEntrypoint, type BuzzMember } from '@ltinteg/app-component-chat'
export function SupportChat({ client, bootstrap, connection }) {
// Slash commands: the `commands` entry point, run as the signed-in person.
const commandsType = bootstrap.entrypoints['commands']
const loadCommands = useCallback(async (actor: BuzzMember) => {
return runEntrypoint({
client,
type: commandsType,
input: { actor_public_key: actor.publicKey }
})
}, [client, commandsType])
const stableBootstrap = useMemo(() => bootstrap, [bootstrap])
return (
<BuzzChat
connection={connection}
client={client}
bootstrap={stableBootstrap}
theme={bootstrap.theme?.document}
loadCommands={commandsType ? loadCommands : undefined}
/>
)
}
bootstrapmust be a stable object (memoize it); the component depends on it.clientturns on the member directory and every entry point the space exposes.loadCommandsturns on the/palette. It receives the actor the person is talking to and returns that actor's commands. The component caches the list per actor; a failed load is not cached. Keep the function memoized so there is one cache per session. When the bootstrap has nocommandskey, the space does not offer commands yet: re-runbuzz.space.enable(see After a platform upgrade) and leaveloadCommandsout until then. Without it,/stays plain text.
Cards need nothing else. The component parses each actor message, draws the card with the built-in renderers, and sends the person's answers back as threaded replies. It draws a card only on a message the member list marks as an attached actor; the same block from a person stays text.
4. Replace or add renderers (uiRenderers)
Every card is drawn by a renderer keyed by name: form, confirm, select, table, actionlist, logs, run, chart, kpi, link, composition, and since 0.9.0 dag, schema, datagrid, query, diff, detail and timeline. The card catalog describes each one. Pass uiRenderers to replace one or add your own:
import { BuzzChat, BuzzUiCardToolbar, type BuzzUiRendererProps } from '@ltinteg/app-component-chat'
function MyKpi({ ui, disabled, sendUiResponse }: BuzzUiRendererProps) {
const tiles = (ui.render?.tiles as Array<{ label: string, value: string | number, unit?: string }>) ?? []
return (
<div className="my-kpis">
{tiles.map(t => <div key={t.label}><small>{t.label}</small><strong>{t.value}{t.unit}</strong></div>)}
<button disabled={disabled} onClick={() => sendUiResponse({ action: 'refresh', values: {} }, { awaitReply: false })}>
Refresh
</button>
</div>
)
}
<BuzzChat connection={connection} client={client} bootstrap={bootstrap} uiRenderers={{ kpi: MyKpi }} />
A renderer receives the parsed card (ui), the person's earlier answer if any, whether it is expired or disabled, whether it is addressed to someone else, and sendUiResponse to answer. The server validates every answer against the card it stored, so a renderer cannot widen what a card accepts. A renderer name you do not register leaves the message as text. The built-in toolbar (BuzzUiCardToolbar) and row actions (BuzzUiRowActions) are exported so your renderer can place them. The fields of each card are listed in Option C and the card catalog.
5. Theme it
The component reads the same theme document as the hosted page (see Theme and customization): brand, colors, radius, density, layout, copy and feature flags. The flag structured_ui (on by default) turns cards on; off, an actor's card shows as its text.
For deeper changes, slots replaces parts of the chat with your own components: Header, ChannelListItem, MessageBubble, EmptyState, ActorBadge, ActorRail, MemberPanel. A custom MessageBubble receives the drawn card as uiContent, so you can place it or draw your own from ui.
Ask your AI assistant
I have the chat component package. Write a React page that signs the user in with @orkestia/auth, bootstraps the chat with fetchChatBootstrap and openBuzzSession, and mounts BuzzChat with loadCommands. Use https://docs.orkestia.dev/raw/chat/option-b-embed-component.md.
Write a custom uiRenderers entry for the table card that draws rows as cards on mobile and keeps the Refresh and row actions working.
My embedded chat shows actor cards as plain text. List what to check: the structured_ui flag, the actor's responder, and whether the message is from an attached actor.
For AI agents
| Rule | Detail |
|---|---|
| Package | Available on request during early access. Do not tell users to install it from a public registry |
| Tokens | The browser uses the end user's own token. Never put an organization token or API key in a client bundle |
| Keys | The chat key is revealed once to openBuzzSession and kept in memory. Do not persist it |
| Commands | Start the commands entry point from bootstrap.entrypoints, with actor_public_key. End users cannot start data.buzz.actor.commands directly |
| Renderers | Custom renderers answer with sendUiResponse. Do not post buzz-ui-response blocks by hand from a person's composer |
Option A: hosted chat with DGI
Step by step, turn on structured chat on the hosted Orkestia chat page. Enable the space, seat and attach an actor, set its DGI responder, choose its reply principal, sync the bridge and republish after upgrades
Option C: custom client (wire contract)
Draw structured chat cards in your own chat client. The buzz-ui block and tag, every renderer and its fields, answers and card actions, server-side validation, in-place edits, quick replies, the status line and slash commands
