Reference
Integrating a frontend
Three things reliably go wrong when wiring a UI to the kernel: the key leaks into the browser, the SSE proxy buffers, and reconnects drop Last-Event-ID so the user never sees the approval. This page is those three, and then the rendering rules.
1. The key stays on your server
A project key has full access to its project — every task, approval, connection and client. There is no task-scoped token to hand a browser. So the browser calls your routes, and your routes call the kernel.
msk_…— machine credential for your product's servermsess_…— member session fromPOST /v1/auth/login, 12 hours, used by the portalmcli_…— client portal session; the only token that may reach a customer, and it only opens/v1/portal/*- Members with several projects must send
X-Mycel-Projecton writes
Your authorisation lives in the proxy. The kernel enforces tenancy; it has no idea which of your users may see which client.
2. Proxy the stream without buffering it
Pass r.body straight through. The moment you await r.text() the live view becomes a spinner that resolves at the end.
// app/api/tasks/[id]/events/route.ts
export async function GET(
req: Request,
ctx: { params: Promise<{ id: string }> },
) {
const { id } = await ctx.params;
const KERNEL = process.env.MYCEL_KERNEL_URL ?? "http://localhost:4000";
const last = req.headers.get("last-event-id");
const r = await fetch(`${KERNEL}/v1/tasks/${id}/events`, {
headers: {
authorization: `Bearer ${process.env.MYCEL_API_KEY}`,
...(last ? { "Last-Event-ID": last } : {}),
},
signal: req.signal,
});
if (!r.ok) return new Response(null, { status: r.status });
return new Response(r.body, {
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache, no-transform",
connection: "keep-alive",
"x-accel-buffering": "no",
},
});
}Note params is a promise and must be awaited. Check r.ok before streaming: a wrong or out-of-scope id returns 404, and forwarding that body as an event stream gives you a connection that looks alive and never speaks.
Compression and buffering middleware in front of your app will hold the stream. Send no-transform and, behind nginx, x-accel-buffering: no. On a serverless platform, check the maximum duration on the route — a run outlasting the function limit is cut off mid-approval, which reads to the user as the product hanging.
3. Reconnect with the last id
Browser EventSource sends Last-Event-ID for you, as long as your proxy forwards it. If you are consuming the stream by hand, track the last id yourself and send it on reconnect. Missing this is how an approval request is lost, and how a task expires while your UI shows nothing.
On reconnect the server replays from the durable log, so there is no window to reconcile. Deduplicate on id if you already applied an event.
Rendering from the stream
| Event | Do |
|---|---|
token.delta | Append text. Never replace |
tool.called / tool.result | Timeline rows. This is what makes the work legible |
progress | Status line. Also carries Missing knowledge: … |
approval.requested | An editable card from data.preview |
artifact.created | Link to GET /v1/artifacts/:id through your proxy |
task.finished | Terminal. On a non-success status read task.error |
| anything unknown | Ignore it. The contract is additive |
Make the approval card editable. A read-only preview turns the highest-value interaction in the product into a yes/no button, and quietly disables the learning loop — Approvals.
Do not forget the gap before the first event: task.created only arrives once the sandbox is up. Render the queued state from the task you got back from POST /v1/tasks.
Serving your customers directly
If you want customers to see their own work rather than going through your UI, use the client plane. Mint a link with POST /v1/clients/:id/portal-link, exchange it once at POST /v1/portal/session, and use the resulting mcli_ token against /v1/portal/*.
Its event stream is filtered: no cost, no token deltas, no approval events. The customer sees the work happening without seeing your margin or the moment you corrected the agent. Links are single-use and expire in seven days; sessions last thirty. Both live in process memory today, so a kernel restart signs everyone out.
Contract over packages
There is no @mycel/react. Use create-mycel-app, or the mycel-workspace skill so your coding agent generates the workspace against the event contract in your own design language. The portal in portal/ is the off-the-shelf operator console if you do not want to build one yet.
What to read next
Configuration — every environment variable, and which of them you have to set before this is real.