Embed a UI

Alpha. @m8tes/react is the React UI layer for the API, published under the alpha dist-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.

TSX

Install

Terminal

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:

Terminal

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:

TSX

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.

TSX

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

TSX

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 call client.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 read runId off 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:

CSS

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:

TSX

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:

  1. Pin the mateagentId for one shared mate, resolveAgentId for 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.
  2. 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.
  3. 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.
  4. Rotate the key from the Developer dashboard if you suspect exposure.
Rate-limit example (in resolveUserId)
TypeScript
Troubleshooting
SymptomCauseFix
Every message returns 401resolveUserId returned null (no signed-in user)Confirm your auth resolves server-side; for an internal tool use { singleTenant: true }.
404 on a run you just createdThe run belongs to a different end-user, or the id is wrongThe proxy scopes every run to resolveUserId; make sure the same user is resolved across create + stream.
First message hangs on "Connecting…" for 30s+Cold sandbox bootExpected on the first message of a session; follow-ups are warm. Re-skin the connecting status so it reads as progress.
Stream never streams behind nginx / a CDNThe proxy response is being bufferedThe handler sets Cache-Control: no-transform + X-Accel-Buffering: no; ensure your edge honors them and doesn't buffer text/event-stream.
npm i @m8tes/react can't find the versionThe package publishes under the alpha dist-tag, not latestInstall npm i @m8tes/react@alpha, or copy the source via the shadcn registry: npx shadcn@latest add https://www.m8tes.ai/r/mate-chat.json.
403 on a requestThe path isn't run-scopedThe proxy only forwards /runs + run sub-routes by design; everything else is 403.

Next: Streaming & Events · Human-in-the-Loop · Users · Quick Start

Was this page helpful?