> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usepatchwork.co/llms.txt
> Use this file to discover all available pages before exploring further.

# React chat widget

> Direct integration: the browser talks to Patchwork with a token your backend signs.

Needs a keypair, a JWKS document, a minting endpoint, and allowed origins set on the workspace. Work through [Direct integration](/guides/loom-direct) first — this example assumes that setup exists.

## Backend: mint a token

```js theme={null}
import jwt from "jsonwebtoken";

app.post("/patchwork/mint", requireUser, (req, res) => {
  // Compose the subject from server session state, never from the request body.
  const subject = `${req.user.id}:${req.session.workspaceId}`;

  const token = jwt.sign(
    {
      iss: process.env.PATCHWORK_API_KEY_ID, // key_… — the public id
      aud: ["patchwork", "your-api"],
      sub: subject,
      conn: process.env.PATCHWORK_CONNECTION_ID,
    },
    process.env.PATCHWORK_PRIVATE_KEY,
    { algorithm: "RS256", expiresIn: 120, keyid: "your-app-2026-01" },
  );

  res.json({ token, expires_in: 120 });
});
```

Two minutes is deliberate. The browser remints as needed, so a leaked token is worth almost nothing.

## Frontend: the SDK

[`@usepatchwork/react`](https://www.npmjs.com/package/@usepatchwork/react) handles minting, realtime subscription, streaming, and the polling fallback.

```tsx theme={null}
import { PatchworkProvider, useAgent } from "@usepatchwork/react";

export function App() {
  return (
    <PatchworkProvider
      url="https://api.usepatchwork.co"
      connection={CONNECTION_ID}
      mint={async () => {
        const res = await fetch("/patchwork/mint", { method: "POST" });
        const { token } = await res.json();
        return token;
      }}
    >
      <Chat />
    </PatchworkProvider>
  );
}

function Chat() {
  const { messages, send, threads, openThread, newChat } = useAgent();
  const [draft, setDraft] = useState("");

  return (
    <div className="chat">
      <ol>
        {messages.map((m) => (
          <li key={m.id} data-role={m.role}>{m.content}</li>
        ))}
      </ol>

      <form onSubmit={(e) => { e.preventDefault(); send(draft); setDraft(""); }}>
        <input value={draft} onChange={(e) => setDraft(e.target.value)} />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}
```

`useAgent` gives you `messages`, `threads`, `send`, `openThread`, and `newChat`. Assistant content streams in as `message.delta` events when realtime is enabled, and falls back to polling when it is not.

## Without the SDK

The browser calls the same endpoints, minting before each:

```js theme={null}
async function authHeaders() {
  const { token } = await fetch("/patchwork/mint", { method: "POST" })
    .then((r) => r.json());
  return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
}

const thread = await fetch("https://api.usepatchwork.co/v1/loom/threads", {
  method: "POST",
  headers: await authHeaders(),
  body: JSON.stringify({ agent: "agent_4f3c..." }),
}).then((r) => r.json());
```

Note there is no `Patchwork-Subject` header here — the subject rides in the token's `sub`, and any subject in the body would be ignored.

## Handling pauses in a UI

A chat UI is the best place to surface a pause, because the user is right there.

* `pending_elicitation` — the agent's question is just another message. Nothing special to do.
* `pending_confirmation` — render `summary` as an approval prompt with approve and reject buttons, then call the confirm endpoint. See [Approving a write](/examples/approving-writes).
* `pending_action` — do the work in the UI, then post the outcome.

## Do not put `sk_…` in the browser

The workspace key can assert any subject in your workspace. In direct integration it never leaves your servers — that is the entire reason the mode exists.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.