A design pattern for shared state
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.
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.
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:
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.
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.
The pattern is three commitments, and none of them is about how anything is built.
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.
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.
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.
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
logged · replayable · undoable · shared
Free to differ
in memory · broadcast · expiring · per-viewer
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.
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.
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.
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.
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:
(state, op) → state function, run by the daemon authoritatively, again to replay the log after a crash, and again by the browser against its replica — the same function, not the same behavior reimplemented.Put an agent on a canvas — three steps
$ npx skills add dglazkov/isocan
Any harness that reads .agents/skills/ — the skill is one file, not a per-vendor copy.
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
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.
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.