canvas
canvas

Architecture

.md

The two halves of canvas, and what travels where.

canvas has two halves: canvas serve, a local server on the host’s machine, and the web app, a static single-page app served from ui.canvas.frebreco.de. There is no canvas backend in between. Each release channel has its own build of the web app, matching its canvas serve: a stable release opens ui.canvas.frebreco.de, a pre-release (@frebreco/canvas@next) opens ui.canvas.frebreco.de/next.

guest ──┐
guest ──┼── trystero (WebRTC) ──▶ host's browser ── WebSocket + token ──▶ canvas serve ──┬── agents (ACP)
guest ──┘ ├── terminals (PTY)
├── files (read-only)
presence (pointers, selections, focus): every browser to every browser └── board tools (MCP)

canvas serve

A Bun server started in the project directory. It:

  • runs agent sessions over the Agent Client Protocol (ACP), one connection per session, so settings can change at any time;
  • runs terminals as PTYs in the project directory;
  • serves files of the shared set, read-only, watching them for changes;
  • gives each agent session an MCP server on loopback with the board tools;
  • persists the room, the board and the sessions in .canvas/ (see below).

It speaks to exactly one kind of client: the host’s browser, over one WebSocket, authenticated with the token from the host link. It knows nothing about guests.

Web app

A React app. It has no server of its own: the host link tells it where canvas serve is, and peers find each other through trystero, which signals over public Nostr relays and then connects browsers directly over WebRTC.

Authority is a star, presence is a mesh

  • The board is a Yjs document. Guests send their board updates to the host only; the host applies what the guest’s access allows and passes it on.
  • Agent threads, terminal output and open files exist only on the host’s side and are mirrored out by the host’s browser.
  • Anything that would run on the host’s machine is a request to the host’s browser, checked against the guest access (Guests).
  • Presence (pointers, selections, frame focus) goes peer to peer, since it carries no authority.

Agent threads

A session is an append-only log of events: prompts with their author, the agent’s output as AG-UI chunks translated from ACP, permission requests and answers. Every browser folds the same log with the same code, so everyone renders an identical thread.

Board tools

When an agent calls a board tool, canvas serve relays the call to the host’s browser, which runs it against the board document like a local edit. Paths are checked against the shared set first. Layout rules (clusters, rows, placement) are pure geometry over frame positions, shared by the tools and the mouse.

Storage

Everything canvas serve keeps lives in <project>/.canvas/:

File Holds
room.json Room id, room key, the host’s key pair, the host token (mode 0600)
board.bin The latest state of the board
sessions/<id>.ndjson One agent session: its settings and its events

Keeping room.json keeps the links valid across restarts. Delete .canvas/ to start over with new links and an empty board.

Stack

Bun, TypeScript, React 19, Vite, Tailwind v4; @agentclientprotocol/sdk, @tanstack/ai-acp; trystero, Yjs, y-protocols awareness, CodeMirror with y-codemirror; xterm.js; @pierre/trees and @pierre/diffs for the files frame.