Presence and collaboration¶
Status: Current | Last reviewed: 2026-09-29
How two engineers editing the same agent see each other's cursors. Drives the avatar stack on agents, workflows, catalogs, and the code-server panel.
What it is¶
collab-gateway (apps/collab-gateway) is a small Go + Node service that:
- Tracks who is viewing what room (presence).
- Brokers Yjs document updates for collaborative editing (planned, partially live).
Rooms are scoped: agent:<id>, workflow:<id>, catalog:<id>, code-server:<workspace>.
Storage¶
| Layer | Where | TTL |
|---|---|---|
| Presence | Redis DB 4: presence:<scope>:<roomId>:<userId> |
45 s |
| Yjs document state | Redis: collab:doc:<scope>:<roomId> |
live; flushed to Postgres |
| Yjs durable | collab_documents Postgres table |
persistent |
Flush cadence: every 5–10 seconds of quiet.
Auth¶
Two paths:
- Browser (ai-manager, surface-app): user has
ai_manager_sessioncookie. Gateway callsPOST /api/auth/collab-verifyon ai-manager with the cookie. ai-manager replies with{ user_id, tenant_id, name }. The sharedCOLLAB_SERVICE_TOKENenv var authenticates gateway → ai-manager. - code-server extension: server-to-server. The pod has env vars
TRUESERVE_USER_ID,TRUESERVE_TENANT_ID,TRUESERVE_USER_NAMEand presentsx-collab-service-token+x-on-behalf-of[-tenant]headers when calling the gateway.
Permission gate¶
Agent and workflow rooms both require AIManager:RunAgents to join and
AIManager:ManageAgents to write. Viewer joins read-only. Builder can edit
both types. Catalog rooms require AIManager:ManageTools. Platform Admin has
all room permissions, while the selected tenant still determines room access.
Fan-out across replicas¶
Gateway runs 2 replicas in cluster. Sticky sessions are NOT required; fan-out happens through Redis pubsub on the collab:fanout channel. A presence update on replica A publishes; replica B picks it up and pushes to its connected clients.
In Vanguard¶
Port 3510. Started by scripts/start-collab-gateway.sh under Overmind.
ai-manager points at it via NEXT_PUBLIC_COLLAB_WS_URL=ws://localhost:3510.
Restart with overmind restart collab-gateway.
Surfaces using it¶
| Surface | Scope | Component |
|---|---|---|
| Agents canvas (ai-manager) | agent:<id> |
PresenceStack + useCollabPresence |
| Workflows canvas (ai-manager) | workflow:<id> |
Same |
| Catalogs page (ai-manager) | catalog:<id> |
Same |
| code-server extension | per workspace | PresenceHub — polls HTTP every 5 s instead of WebSocket |
Yjs (planned)¶
Phase 1 is presence-only — the avatar stacks. Phase 2 adds Yjs CRDT documents, allowing two users to edit the same agent's prompt simultaneously without conflict. The plumbing (Redis storage keys, Postgres flush) is in place; the FlowEditor and WorkflowGraphTab bindings are not yet wired.
Room reset and hard purge¶
ai-manager uses collab-gateway's internal service-token endpoints to keep
draft state aligned with Postgres. POST /reset/:scope/:roomId rehydrates a
live Y.Doc from the database, or clears the Redis cache when no room is live.
Use that after normal saves or generation writes where the room should continue
to exist.
POST /purge/:scope/:roomId is stronger. It tears down any live in-memory room,
fan-outs the purge to other gateway replicas, deletes Redis state, and removes
the durable collab_documents row. Agent and draft deletion call purge so a
reused agent id cannot hydrate stale canvas state from an earlier deleted
draft.
Reset does not survive an open editor tab — after a hand-repair of a draft row,
use purge. Reset fixes the server's Y.Doc and broadcasts the delta, but a
browser with the agent open holds its own copy in React state and reconciles it
back, re-writing whatever the inbound leaf-patch didn't absorb — a deleted
top-level key comes back, and a nested value reverts, on a flush timestamped
after the reset. /flush-state reports dirty: false throughout, so there is
no signal. Purge tears the room down so clients rejoin against a doc freshly
hydrated from Postgres, and the repair holds.