A design pattern for shared state

Isomorphic
Surfaces

Two surfaces are isomorphic when everything you can do with a click you can do with a command — and neither can see a state the other can't. The guarantee doesn't live in either interface. It lives in the vocabulary they're both restricted to.

01 · The sceneTwo hands on one canvas

A person drags an item across a canvas. At the same moment, in a terminal on the same machine, an agent stacks a new version onto that item. Neither waits for the other, and both changes appear on both screens at once, with the right name on each.

A shared document with two front ends is ordinary; what isn't is the claim underneath — that no feature belongs to the mouse alone — and a claim like that stays true only if something structural keeps it true.

A HAND THE WORD A TERMINAL NOT BUILT YET drag a card drop a file on it pin a note to it type in the thread item.move item.addVersion thread.create thread.reply isocan mv … isocan edit … isocan comment add … isocan comment reply … a tool call? a phone gesture? a voice note? a chat message? A new surface doesn't need a plan. It needs the column. speak the same words and you arrive complete — no re-deriving what “move” means
Capability lives in the middle column. A gesture and a command are two spellings of one word, and the room on the right is the point: anything that learns to say those words inherits all of it at once.

Isomorphic is borrowed from mathematics: a structure-preserving map that runs both ways. Between interfaces it means the set of things each one can express is the same set, acting on one state. Not similar — the same. Two nearby ideas it is not:

Not this

Isomorphic JavaScript

That term means one codebase running on both server and client — a deployment property. This is about capability parity across interfaces, a different axis entirely. The code need not be shared; the vocabulary must be.

Not this

A CLI wrapper on an app

A wrapper mirrors whatever subset someone got around to mirroring. The app team ships a feature; the CLI team catches up; the gap becomes a backlog item that never reaches zero. Parity by effort always decays.

Isomorphism is not a property of your interfaces. It is a property of the vocabulary you refuse to let them escape.

02 · The patternName the changes, not the screens

The pattern is three commitments, and none of them is about how anything is built.

  1. Enumerate what can change

    Every change the state can undergo is a named value in one closed set — defined before any interface exists, and belonging to none of them. Not an API per client. One list.

  2. Give surfaces no other way in

    An interface's only power is to author values from that set. A drag is one way to say "move this"; a command is another. Neither has a private door, so there is nothing for one to have that the other lacks.

  3. Keep one state, and tell everyone

    Changes are applied in one place, to one state, and every surface is told. Parity of capability without parity of view is only half the property — both surfaces have to be looking at the same thing.

What follows from that is the useful part: the set is now the definition of the product. A capability that isn't in it doesn't exist for anyone, and one that is in it exists for everyone — including surfaces nobody has built yet.

03 · The lineWhat is deliberately not isomorphic

The pattern is usually explained as "make everything symmetric," which is wrong and makes it unbuildable. Isomorphism binds the durable plane — the state everyone shares and the record that proves it. The ephemeral plane is where surfaces are allowed to be themselves, and drawing that line in the right place is the actual design work.

Bound by the contract

The durable plane

logged · replayable · undoable · shared

  • Content and structure — what exists, where, in what version
  • Conversation — threads, replies, what they're pinned to
  • Attribution — the identity stamped on every change
  • History — the record itself, and how to walk it back

Free to differ

The ephemeral plane

in memory · broadcast · expiring · per-viewer

  • Presence — cursors, sessions, "what I'm doing right now," expiring on a timeout so a crashed client doesn't haunt the canvas
  • Viewport — where each surface is looking, at what zoom
  • Read state — what you have seen, kept by you. Which threads you have read stays in the browser; a coarser mark — the canvas, the point you had reached, and when — is kept durably by the home so it follows you between machines. Still never anyone else's to read: there are no read receipts here, and adding them would take a route that does not exist.
  • Undo scope — shared state, but each actor's undo walks only their own changes

Isomorphism binds the durable plane. The ephemeral plane is where a terminal gets to be a terminal.

That last row is the subtle one. Undo is usually thought of as a property of a document, but in a shared document a global undo stack means your last action can be erased by someone else's reflex. Actor-scoped undo keeps the state shared and the history personal — asymmetric on purpose, because symmetry there would be a bug.

04 · The consequenceThe contract is a seam teams can build against

Once the vocabulary is the contract rather than any one client, it becomes an organizational boundary. The interface team and the agent-surface team each build against the set of changes, not against each other's release schedule. Neither is downstream.

05 · Why nowYour collaborator might not have a mouse

The pattern has always been good hygiene. What makes it urgent is that software now has a second kind of user, and that user is most fluent in text.

An agent can drive a mouse: read the screen, find the button, click it. But that is a reconstruction — state inferred from pixels, intent squeezed through an interface built for hands, and the work reported back in prose because there is no other channel. It often succeeds, and it is lossy in every direction.

An agent pointed at an isomorphic surface issues the same changes the interface issues. Its work appears on the human's screen as it happens, attributed and undoable, indistinguishable in kind from a collaborator's. There is no "let me tell you what I did," because you watched.

Granularity is freedom

The deeper effect is where the line gets drawn. Without a vocabulary, every capability an agent is given is a separate judgment call — someone decides which task deserves an entry point — and that line is almost always drawn too high. You end up exposing a handful of coarse, task-shaped hooks: generate the summary, publish the page. The agent can do those things and nothing between them.

Enumerating changes at their natural grain removes the judgment. The line falls where the state's own joints are, which is as low as it goes, and the agent composes from primitives instead of petitioning for a feature. It can then do things nobody designed a hook for — which is exactly what you want from a collaborator, and exactly what a coarse interface forbids.

A surface only an agent inhabits is a house it lives in. A surface both can drive is a stage they dance on.

That is the shift worth naming. Building for agents usually means building a container — a room the agent works inside, whose walls are whatever hooks it was handed, and whose output is a report. An isomorphic surface isn't a container. It is a shared place, where the human and the agent make changes of the same kind, in the same units, in front of each other. The prize isn't automation. It is that presence becomes truthful, and a machine collaborator becomes a peer rather than a process you supervise.

06 · Working exampleisocan

isocan — short for isomorphic canvas — is an infinite canvas built to hold the claim rather than describe it. A local daemon owns the state; the web app and the isocan CLI are equal clients over it. Every change is a value from one set, posted to one endpoint, applied by one function that the daemon runs authoritatively and the browser runs against a replica.

// the contract, abridged — packages/core/src/ops.ts
export type Operation =
  | { type: "item.add"; itemId; version; width; height; placement }
  | { type: "item.move"; itemId; x; y }
  | { type: "item.addVersion"; itemId; version }
  | { type: "thread.create"; threadId; x; y; anchorItemId; comment }
  | { type: "thread.reply"; threadId; comment }
  // …every mutation either surface can perform, and nothing else

If it isn't here, no surface can do it. If it is here, both can.

Four implementation choices hold the three commitments in place — the pattern doesn't require these, but something has to play their part:

Put an agent on a canvas — three steps

  1. Add the skill to your project. $ npx skills add dglazkov/isocan
  2. Start your agent there.

    Any harness that reads .agents/skills/ — the skill is one file, not a per-vendor copy.

  3. Tell it: use isocan

    It installs the CLI and starts the daemon itself, picks a name of its own, and hands you the URL. You open the app, pick your name, and make the canvas — the agent deliberately doesn't, so no canvas is stamped with whoever typed the command.

What the pattern buys, in practice

An agent that is visibly in the room

Because presence is a first-class ephemeral plane, an agent working through the CLI shows up as a named cursor that moves to wherever its last change landed — and a comment thread is the channel it answers on. The human watches the work arrive rather than reading a report about it.

Because the state is durable and the vocabulary complete, that same agent can be handed the canvas cold: it lists what's there, reads the threads, edits an item into a new version, and replies — every step something the human could have done by hand, and can see, and can undo.

This page is one of those artifacts. It was built on an isocan canvas: the outline argued out in a comment thread, the diagram chosen from variants pinned side by side, the design system picked the same way, each round landing as a new version on the same item while the other party watched it arrive. Then it was committed here.

An isocan canvas: the main thread docked at the left carrying the conversation that produced this page, the drafts and diagram variants spread across the canvas, and an agent's cursor parked mid-canvas, labelled 'Kenny — waiting for you…'
This page, mid-argument. The docked panel at the left is the main thread — the whole conversation that produced what you are reading, one surface talking to the other. The drafts, the design-system mocks and the nine diagram variants are laid out beside it; the tag in the middle is the agent's cursor, parked: waiting for you…

The pattern generalizes past canvases. Anything with shared state and more than one kind of user — editors, dashboards, issue trackers, design tools — can be built this way. What it costs is the discipline to name the changes first and route everything through them. What it buys is that the second surface is never the lesser one.