Skip to content

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:

  1. Tracks who is viewing what room (presence).
  2. 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_session cookie. Gateway calls POST /api/auth/collab-verify on ai-manager with the cookie. ai-manager replies with { user_id, tenant_id, name }. The shared COLLAB_SERVICE_TOKEN env var authenticates gateway → ai-manager.
  • code-server extension: server-to-server. The pod has env vars TRUESERVE_USER_ID, TRUESERVE_TENANT_ID, TRUESERVE_USER_NAME and presents x-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.

See also