The problem
Architecture gets settled in conversation and lost there. What survives a whiteboard session is a photograph, and what an implementation agent is handed afterwards is prose that omits exactly what matters — the component boundaries, the data model, the invariants that must not break. Generated code drifts because nothing pins those down, not because the model is weak.
The design surface and the specification are also usually two different tools, which means they disagree within a week of each other. This collapses them into one: the canvas is the source of truth, and the specification is generated from the graph, so the document cannot describe a system the diagram no longer shows.
What I did
- Schema and tenancy
- PostgreSQL through Prisma. Projects key to the Clerk user id and carry a status enum, with indexes on owner and creation date so the project list stays one indexed read. Collaborators are a cascading relation with a composite unique on project and email — an invitation exists before the invitee has an account, and resolves to a profile through Clerk's backend API at read time. The Prisma client is a cached singleton that selects its adapter from the connection string, so pooled and direct Postgres run the same code.
- Room authorisation
- A token endpoint in front of every Liveblocks room. Rooms are reachable by id, so the socket handshake is the boundary, not the UI: the endpoint resolves the caller through Clerk, checks ownership or collaborator membership against the project row, and issues a scoped token or refuses. Invite and remove are enforced server-side on the same principle.
- Shared canvas state
- React Flow over a Liveblocks storage document, so nodes, edges, presence and cursors are one conflict-free shared structure instead of local state pushed across a socket. The agent uses the same primitives as a human — its cursor and thinking state are presence fields, not a bespoke channel.
- The generation pipeline
- A POST route authenticates, writes a task-run row binding the run id to project and user for later ownership checks, then hands off to a Trigger.dev task. The task prompts Gemini through the Vercel AI SDK against a schema, so the model returns a validated array of canvas operations — add, move, resize, update, delete — rather than prose to parse. The whole array is applied in one atomic storage mutation, and the client subscribes to the run over Trigger's React hooks instead of polling for progress.
- Specification generation
- A second model pass that serialises the canvas graph — services, stores, edges and their labels — into a structured markdown specification, stored against the project and listed in the workspace panel.
How it works
On screen

Owned and shared projects come from different reads — ownership from the project row, shared through the collaborator join — and carry different permissions. 
A background run mid-flight. The agent holds presence in the room, and its operations land on every connected client through the same storage mutation a human edit uses. 
The graph serialised into a markdown specification, stored against the project — the artefact an implementation agent is pointed at.
Decisions
How should the AI put anything on the canvas?
ChoseIt answers with typed canvas operations
Instead ofIt answers with a diagram, or with prose to parse
Ask a model for a picture and you get something to look at and nothing to edit. Constraining it to a schema — add this node, move that one, connect these two — means the output validates before it is applied, and lands in the same structure a human drag produces. The agent's edits and the team's edits become the same kind of object, replayable through the same mutation path, and everything it draws stays editable afterwards.
Who is allowed into a room?
ChoseMembership is checked server-side, and only then is a room token issued
Instead ofHiding rooms the user should not see
Real-time rooms are open by default — anything holding a room id can try to connect. So access cannot be a question of which buttons the interface renders. The server confirms the person owns the project or was invited to it, and the socket opens only after that.
Where should a slow AI run live?
ChoseA background job the browser subscribes to
Instead ofDoing the work inside the request
A generation takes far longer than a request should stay open, and it has to survive the person who started it closing the tab. Run it in the background and stream its status into the room, and the whole team watches one run — not just whoever pressed the button.
Must a collaborator already be a user?
ChoseInvitations are keyed by email address
Instead ofA foreign key to an existing user record
Requiring an account before an invitation can exist means you can only invite people who have already signed up, which is backwards. The email is the key. Identity is resolved when the list is read, and degrades to showing just the address while there is no account behind it.
Where it got to
- The stack
- Next.js and React 19 with TypeScript throughout, Prisma over PostgreSQL, Clerk for identity, Liveblocks for shared state and presence, Trigger.dev for background runs, and the Vercel AI SDK against Gemini. Deployed on Vercel.
- The concurrency model
- Any number of humans and the agent edit one document. Every write is an atomic mutation on shared storage, every client is pushed to rather than polling, and the agent is not a special case — it takes the same path a human edit takes.
- The authorisation boundary
- Identity resolves on the server for every privileged path — room tokens, invitation, removal. The client renders permissions; it never decides them.