Embed a UI
Alpha.
@m8tes/reactis the React UI layer for the API, published under thealphadist-tag (npm i @m8tes/react@alpha). Component and hook APIs may change before 1.0; the streaming wire protocol (m8tes.stream.v2) stays semver-stable.
Put a live agent in your own React app: streaming replies, tool calls, and inline human-in-the-loop approvals, themed to your product. Two pieces, and that's all the wiring there is: the component, and a one-file server route so your secret key never reaches the browser.
Install
The server-only handler lives behind a separate @m8tes/react/server import, so the secret can never leak into a client bundle.
Prefer to own the source? The shadcn registry copies the component into your repo, like any other shadcn component, so you can edit anything:
Add the server route (your key stays server-side)
There's no browser-safe key, so a route on your server holds the m8_ secret and forwards stream requests. In Next.js (App Router), one file does it:
The handler is locked down by design: it forwards only the run-scoped endpoints and 403s everything else, strips any client-supplied user_id and injects the one resolveUserId returns, and drops permission_mode, tools, instructions, and model from every body. resolveUserId is required (fail-closed; null → 401): pass { singleTenant: true } only if every user should share one scope.
Wiring your auth: Clerk, Auth.js, Supabase, Express
resolveUserId receives the incoming request, so map it to your authenticated user however you already do. Never derive the user id from a client-supplied field: the proxy strips it precisely so the browser can't spoof another user.
Non-Next frameworks (Express, Hono, Remix) use createM8tesProxy, which returns a framework-agnostic (Request) => Promise<Response> handler you adapt to your server. Full adapters: auth-adapters.md inside the package.
Render the agent: <MateChat>
<MateChat> is the full panel: thread, composer, streaming markdown, tool calls, and the approval / question gates. The client parses the raw V2 SSE wire format, reconnects on drops, and de-duplicates replayed events, so you render messages, not byte streams (Streaming).
Human-in-the-loop is built in: when a run pauses for approval or a question, an inline card appears in the thread and your answer resumes the run. Pausing is governed by your mate's permission mode, not the browser: the proxy never lets a client set the run's permission mode. If a run fails or is cancelled, a Run failed · Retry row re-sends the last message on the same run.
First-message latency: runs execute in an isolated cloud sandbox, and the first message of a session boots it (a few to tens of seconds); follow-ups are warm. <MateChat> shows a "Connecting…" status during boot; classNames.statusBar restyles that row so a cold start reads as progress.
Threads across a refresh. The panel holds no history of its own — reload the page and it starts empty. Pass a runId to rejoin an existing thread.
Getting that id back is the part <MateChat> can't do for you: it accepts runId but exposes no callback for the run it just created. Two ways to obtain one:
- Server-side (simplest). The proxy deliberately doesn't forward
GET /runs— that would let a browser enumerate runs — so callclient.runs.list(user_id=…)from your own backend and hand the browser the ids it may open. This is also how you build a thread list. - Headless. Drive
useMate()(level 4 below) and readrunIdoff the hook, then persist it yourself.
Make it yours
Four levels, lightest first; most apps stop at level 1 or 2.
1. Theme tokens. The chat reads your shadcn/Tailwind variables first and falls back to the m8tes palette, so in a shadcn app it matches your product automatically. Override anywhere above the chat:
2. classNames. Restyle individual slots: <MateChat classNames={{ root: "rounded-2xl shadow-lg" }} />
3. components. Swap any rendered piece: bring your own avatar, tool-call card, approval dialog, or markdown renderer.
4. Headless. Skip <MateChat> and drive the hook; you render the rest:
The package ships a Customizing <MateChat> reference at docs/customizing.md (open it from node_modules/@m8tes/react/docs/customizing.md after install) with the full token table, dark mode (works under a .dark ancestor), the --m8-* state-accent tokens, the Tailwind v3 bridge, and every slot and component override.
Security model (multi-tenant)
The proxy enforces: the key never reaches the browser; only the run-scoped endpoints are forwarded (a compromised session can't reconfigure your account); user_id is injected server-side so user A can never read user B's runs; and no policy escalation from the browser. What stays yours:
- Pin the mate —
agentIdfor one shared mate,resolveAgentIdfor one mate per end-user (shown in the route snippet). Without a pin, an end-user can start runs on any non-Company-Agent mate in your account. - Scope the mate's connections to the end-user, not the account (per-user OAuth). An account-level connection (your Gmail, your Stripe) means a prompt-injected end-user could act on your behalf; in an embed, the person clicking Approve is the end-user.
- Rate-limit in your route (every message is a run billed to your account) and cap your spend with a usage overage ceiling plus per-end-user sub-caps (per-user overrides); when an end-user hits one,
<MateChat>shows a "limit reached" card. - Rotate the key from the Developer dashboard if you suspect exposure.
Rate-limit example (in resolveUserId)
Troubleshooting
Next: Streaming & Events · Human-in-the-Loop · Users · Quick Start
