Every collaborative editor demo hides the same lie: the hard part isn't the editor, it's the infrastructure underneath. Durable state per document. Change fan-out to every participant. Presence. Reconnect logic. That's why products like Liveblocks exist — and why Cloudflare built an entire primitive (Durable Objects) around "one stateful object per document."
I wanted to see how much of that stack I could get on Telnyx Edge Compute — and then bolt an AI copilot on top that watches the document and proposes edits, without managing a single API key.
The Telnyx code example is here:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/collaborative-doc-ai-copilot
The Insight: The Actor IS the Document
The whole design hangs on one decision: the actor id is the document id.
env.DOCS.idFromName(docId) // one durable, single-threaded actor per doc
That's the Cloudflare Durable Objects model — one stateful island per document, serializing all writes to that document while other documents run in parallel. Text, cursor presence, and pending AI suggestions all live as durable merge-patch state on the actor. Restart the function; the document is still there.
Inside the actor, the state machine is small:
export class DocActor extends Agent<Env, DocState> {
protected initialState(): DocState {
return { text: "", cursors: {}, suggestions: [], lastSuggestionAt: 0 };
}
protected async onStateChanged(next: DocState, prev: DocState): Promise<void> {
this.sockets?.broadcastSnapshot(next); // fan-out to every watcher
if (next.text !== prev.text) {
await this.queue("runCopilot"); // own actor turn
}
}
}
onStateChanged is the hinge of the whole sample. Every durable change broadcasts to all participants, and a text change queues the copilot — as its own turn, so LLM latency never blocks anyone's keystrokes.
Multiplayer I Didn't Write
The part that surprised me most: there is no fan-out code in this sample. The Agent SDK ships a socket layer — AgentSocketServer on the actor side, AgentClient in the browser — and it does the hard parts:
- State snapshot +
helloon connect - New state pushed to every watcher on every
setState - Inbound
callframes dispatched to the actor's public methods (typed RPC over the socket) - Reconnect with exponential backoff, heartbeats, ping timeouts
The browser is ~20 lines:
import { AgentClient } from "@telnyx/edge-runtime/client";
const client = new AgentClient(`wss://host/websocket?doc=demo&name=Alice`);
client.onState((state) => render(state));
await client.stub.edit("Alice", "new text"); // typed RPC
await client.stub.respondSuggestion(id, true); // accept a suggestion
Even presence is just state: cursors live in a Record<name, position> on the actor, deleted (merge-patch null) when a socket closes. The participant chips are Object.keys(state.cursors).
The Copilot: Thirty Lines, Zero Credentials
When the text changes, a queued runCopilot task runs as its own actor turn and calls Telnyx Inference through the pre-authenticated binding:
const completion = await this.env.TELNYX.ai.openai.chat.createCompletion({
model: "meta-llama/Llama-3.3-70B-Instruct",
messages: [
{ role: "system", content: COPILOT_SYSTEM_PROMPT },
{ role: "user", content: `Document content:\n\n${state.text}` },
],
});
This is the part I keep re-noticing on Telnyx Edge: this.env.TELNYX is already authenticated. No apiKey field, no secret rotation, no key accidentally shipped to the browser. The copilot's suggestion goes into actor state, broadcasts like any other change, and everyone gets an Accept / Reject card. Accept rewrites the document for everyone, attributed to the copilot.
Rate limiting is per document — a cooldown timestamp reserved before the LLM call, so a burst of typing can't stampede inference.
Running It
npm install && cp .env.example .env # API key for local dev only
npm run local:dev
Two browser windows — ?doc=demo&name=Alice and ?name=Sam. Type in one, watch the other, wait five seconds, accept the suggestion. For real deploys: telnyx-edge new-func --actor, merge the telnyx.toml bindings, telnyx-edge types, telnyx-edge ship — and no API key follows the function.
What I'd Change for Production
Honesty section, because every collab demo needs one:
- The protocol sends full-text replacements. Demo-size documents: fine. Real documents: move to CRDTs (Yjs) and keep the copilot trigger on state changes.
- There's no auth — any
?name=joins. Gate the WebSocket upgrade path. - The copilot prompt is "rewrite the doc." Tune model, prompt, and token budget per use case — a "suggest improvements as inline comments" variant is a system-prompt change.
The Takeaway
The infrastructure that made collaborative editing a product category — durable per-document state, fan-out, presence, reconnects — is now platform surface. The actor-per-document model gets you the Durable Objects isolation story, the socket layer deletes your fan-out code, and the AI feature on top is small enough to read in one sitting. The zero-credential binding is what makes it feel less like a demo and more like something you'd actually ship.
Top comments (0)