WordInWeb · Collaboration
What does an RTS game have to do with a collaborative DOCX editor?
Secure lockstep architecture with intent-based simulations for multiple clients and an ultra-lightweight backend. Or something like that. Very thrilling title.
July 30, 2026
My hypothesis was that a super lightweight server could receive edit intents and serve valid intents to everybody working on a document.
I recently built a DOCX editor that runs in the browser. It parses OOXML, lays out pages, and edits the document without converting it to another format.
Then I made it collaborative. Colla Word In Web! Collab WIW going forward.
The obvious solution here is to use operational transform like Google Docs does, or maybe a CRDT like other web-first DOCX editors use. But I wanted the server to be super lightweight, so no CRDT. I also wanted the server to be non-custodial and end-to-end encrypted. I don’t want your data, sorry.
TLDR
Collab WIW is a WYSIWYG DOCX editor with MS Word parity that supports real-time collaborative editing. Its collaboration layer is end-to-end encrypted and uses ephemeral rooms with stable share URLs. The server keeps each live room in memory, while every browser keeps a durable copy of the document and its edit history. A saved browser copy can bring the room back online at the same URL and reconcile offline edits when collaborators return.
Definitions
Some quick ones:
- Node: One item in the document outline. A paragraph is a node. A table is a node. A formatted piece of text is a node.
- Text run: A piece of text with the same formatting. In “Hello world,” “Hello” and “world” are separate runs if “world” is bold.
The “RTS” part
My inspiration was OpenFront! GOATED game. OpenFront serves as the authoritative game state for all clients. Each client sends an intent, and the server decides whether that intent is valid. The game state is a deterministic simulation based on the intent history.
Collab WIW follows the same shape:
- A user applies an edit, and it renders immediately for them.
- The browser sends the intent to the collaboration server.
- The server assigns the next sequence number.
- Every browser processes the same ordered stream.
- Every browser derives the same document.
Basically, it is a deterministic state machine applied to a word processor.
Lockstep architecture
The idea for E2EE came later. I started with a functional collaborative editor, so there are two authority paths in the collaboration package: plaintext mode and encrypted mode. The demo uses encrypted mode, so that is what I am going to write about from here.
Plaintext mode is straightforward and works more like traditional Operational Transform. The server runs DocumentSession. It transforms, validates, applies, and broadcasts each edit.
In encrypted mode, the server is a totally blind sequencer. It checks the visible envelope metadata and clear abuse, assigns a sequence number, and relays the ciphertext to the other browsers in the session.
When users in a Collab room receive an intent from the server, their browser:
- decrypts the padded envelope;
- feeds it into the local
DocumentSessionmirror; - runs the same transform, intent-validation, and apply pipeline;
- sends the canonical result to the visible editor replica.
Basically, intent validation gets offloaded to each local browser.
OOXML is a structured, addressable tree. All the server really has to do is make sure every client sees the same encrypted envelopes in the same order.
Editing conflicts, race conditions, and contention resolution
A scenario: I edit character 400 while somebody else deletes a paragraph near the start. By the time my edit arrives, character 400 could mean something totally different.
Collab WIW addresses content with stable IDs for the paragraph, table, and text run. Each character offset is local to that run:
paragraph id + run id + offset
The IDs live in a sidecar table.
So what is the sidecar table?
OOXML does not give every paragraph, table, and text section a positional stable ID that I can safely use over the wire. I also cannot say “the third run in the second paragraph,” because that stops being true when somebody edits above it.
Collab WIW keeps a tiny lookup table next to the DOCX:
stable ID -> current path through the OOXML tree
It also stores the next ID that can be assigned. All browsers create the initial table with the same document-order walk. When an edit creates a new node, the browser that made the edit sets the ID and includes it in the intent. Every other browser processes that exact value.
The sidecar lives next to the DOCX instead of inside it. Collaboration metadata stays out of the OOXML, which keeps DOCX round trips clean. In encrypted rooms the sidecar gets sealed into the checkpoint with the DOCX, so the server cannot read it either.
When a browser restores a checkpoint, it loads the DOCX and reapplies the sidecar. If an intent targets run 42, run 42 means the same thing in every browser.
Stable IDs solve most false conflicts. Edits in different paragraphs usually have no positional relationship to resolve.
The OT-lite architecture
Google Docs is the peak editing experience for collaborative editing. It is about as seamless as it gets, but that is because a central server handles the document and broadcasts that state to everybody. That is the correct way to do it. There is no good reason for your browser to manually lay out complex OOXML into the DOM. But I want to do that!
There is still a good deal of complexity. If I insert three characters before your caret, your offset must move by three. If I delete the range that contains your caret, your position must collapse to the deletion point.
Collab WIW handles that with a small transform over run-local offsets. I have been calling it OT-lite.
The sidecar tells every browser which paragraph and piece of text an edit refers to. The transform has a much smaller job:
What did the edits that landed ahead of mine do to the character position I was aiming at?
That same little bit of math gets reused everywhere: on the plaintext server, inside the hidden mirror for encrypted rooms, when optimistic typing replays an edit, and when somebody hits undo.
This is where I had to correct my first idea. I originally thought deterministic OOXML layout was enough to stop clients from drifting. Two browsers can lay out a page exactly the same way and still apply an edit to two different pieces of text.
What keeps everybody together is much simpler: the same stable IDs, the same edit order, and the same deterministic editing code.
Optimistic typing
The browser applies a local edit before the server round trip. Its replica keeps two related states:
- the confirmed document contains the canonical sequence;
- the visible document adds local pending edits on top.
When you type, Collab WIW applies the edit to the visible document first and sends it in parallel. If an earlier remote edit changed its target, the client rolls back the state, transforms the pending edit, and replays it so the document stays visually synced.
The server remains authoritative about order. The browser remains responsive about presentation.
Pretty standard, more or less what all editors do.
Performance
Can it run Doom? IDK, maybe? IT CAN let multiple collaborators work on documents as large as 500 pages.
Large-document performance depends on the amount of work attached to each edit. The viewer mounts a small window of pages. A text intent carries its affected paragraph through the replica and rendering layers. The editor reparses that paragraph, lays out the affected pages, and patches compatible page elements in place. The more things affected, the more the page has to recalculate. More on this in the other blog. Also, documents this big CHUG in Word on my computer.
Undo keeps one reference copy of the document at the last undo point. After you type, the editor compares the live document with that copy and saves the before-and-after text for the pieces that changed.
When you hit Undo, it swaps those pieces back. Bigger changes, like inserting a table or changing the document structure, use a full snapshot because the whole shape of the document changed.
When the browser compresses a recovery copy, it sends that work to a background worker so the editor can keep responding. The same idea applies to encrypting checkpoints, checking that browsers agree, and preparing browser saves. Each background task carries the edit number where it started, so the result returns to the correct place in the sequence.
TLDR: a benchmark on a 12,000-paragraph DOCX, or about 500 pages.
| Mode | Median | p99 | Keystrokes painted |
|---|---|---|---|
| Local editing | 7.1 ms | 13.7 ms | 100/100 |
| End-to-end encrypted collaboration | 7.6 ms | 20.0 ms | 100/100 |
After garbage collection, the JavaScript heap settled near 162 MB locally and 182 MB collaboratively, which is not terrible. I think I started this project with it sitting at about 2 GB.
Edits that grew the document from 500 to 501 pages triggered full repagination and took 171 ms. That is not as great, but it was a tradeoff we made early when we chose how to render the DOM. Maybe I can draw it to a canvas and hyper-optimize performance later. But that is a later problem.
Rejoining, offline work, and stable URLs
A big part of any collaborative editor is reconciling offline or disjoint edits. So if the server is ephemeral, how does all of that work when somebody joins late or brings an expired room back online?
For late joiners, their browser starts from a recent checkpoint and only replays the edits that came after it.
The first browser starts the room with one encrypted snapshot of the full DOCX. That snapshot includes the document and the stable-ID sidecar at a specific edit number. That is the checkpoint.
After that, the server keeps two things in memory:
- the latest encrypted checkpoint;
- the ordered encrypted edits that came after it.
When somebody joins, their browser downloads both, decrypts the checkpoint, restores the sidecar, and replays the newer edits in order. They end up at the same current document after replaying only the latest part of the room history.
One browser in the room gets assigned checkpoint duty. At certain edit numbers it takes the confirmed document from its local mirror, captures the sidecar from that same state, and encrypts them together. The server accepts that checkpoint when its edit number matches the current end of the room. It can then throw away the older encrypted edits already included in that checkpoint.
The checkpoint code generates the sidecar from the confirmed mirror. It walks the current document, records each stable ID’s current location, and puts that table with the DOCX at the same edit number.
The server keeps the live room state in RAM. A server restart clears the room. The durable copies live in the browsers, so anybody who still has one can bring the document back online.
Each browser saves a recovery bundle in IndexedDB. It includes:
- the confirmed DOCX;
- the sidecar;
- the last confirmed edit number;
- edits waiting for confirmation;
- edits made while offline;
- the room version where those offline edits started;
- media references;
- enough document history to tell whether one saved copy came before another.
That same browser storage powers autosave, saved documents, recent versions, recovery drafts, and old copies from previous room versions. The demo also tells you when browser storage fails and cleans up abandoned copies during startup.
You can keep editing when the server disappears. Each edit applies to your local document and gets added to an offline list in browser storage. That list also records which version of the room you were editing.
When the connection comes back, there are a few possibilities:
- The same room is still alive. Your browser downloads the edits it missed, resends anything waiting for confirmation, and ignores duplicate replies by matching each edit’s client ID and client-side edit number.
- The same room is still alive and you made offline edits. Your browser catches up with the room first, then sends your offline edits through the normal collaboration path one at a time. Each one gets transformed against the edits that landed while you were away.
- Somebody restarted the document in a new room and your saved copy is an earlier point in that same history. Your browser fast-forwards to the new room and keeps the older copy as a recoverable version.
- Somebody restarted the document while you also made offline edits. Now there are two real branches. Collab WIW saves your full offline document as a draft and offers the text edits as tracked suggestions with your name attached. The draft preserves bigger structural changes exactly as your browser saved them.
- The two copies belong to separate saved histories. Collab WIW keeps the full local copy as a draft and opens the live room as its own document. Both copies survive.
Offline edits replay one at a time. After each edit gets confirmed, the browser removes it from the saved offline list. If the tab closes halfway through, the next session starts with the edits that remain.
How the stable URL works
A stable URL is the address for one collaboration:
https://collab.word-in-web.com/?doc=d_...#k=...
The doc value is a random 128-bit document ID. It stays the same every time that document comes back online.
Each live room gets its own random room version, called a genesisId. The browser derives fresh encryption keys from the document key and that room version. It also binds every checkpoint and edit to the document ID, room version, and edit number. That keeps an old encrypted edit attached to the exact room and position where it came from.
When the server dies or the room expires, a browser with a saved bundle can seed a fresh room under the same document ID. The document gets a new room version, while the original share URL still works. If two browsers try this at the same time, the first seed wins and the second browser joins it.
The random document ID makes guessing a room address basically impossible, but sometimes links leak.
So what if somebody yoinks your room’s link and tries to mess with your stable URL? Technically, they can use their own client to seed a different document under an empty document ID. Assume the server already cleared the old room, so it has no previous checkpoint to compare against. It accepts the first new seed and gives it a fresh room version. A share code protects access to a live room, but the current revival request can choose a new code for the new room version.
This can take over the old collaboration URL, but it obviously cannot erase the copies already saved in other browsers. When an old participant returns, their browser sees that the room version changed and saves its previous document as a draft before it opens the new room.
The person then resolves the conflict. Small offline text edits can come into the live document as tracked suggestions. Structural edits and completely different documents stay as DRAFTS. If the new room contains an unrelated document, the old participant can open their saved draft, put it online under a new document ID, and share the new URL.
Moving to that new stable URL is quick in the demo. Like one button press.
Media
Images are way larger than normal edits, and a large image clogging up the server is suboptimal. But media can load asynchronously. We only need to know where it goes and how large it is. The actual image can load later.
The intent says where the image belongs, how big it is, and which encrypted blob to request. The encrypted image uploads over HTTP while the document keeps moving through the normal WebSocket sequence.
The relay keeps recently used images in RAM. Older images can move into an encrypted temporary disk cache. That cache gets a fresh key every time the server starts, and startup clears anything left from the previous run. If a server dies while media is on disk, the key disappears because it only existed in RAM. The server shreds the remaining files on boot.
The image address is a hash of the encrypted bytes. That lets the relay confirm that an upload matches the requested blob while the pixels stay encrypted. It also prevents media swaps in flight.
Eventually the cache drops the image. A late joiner can ask the other people in the room for it. A browser that still has the image uploads the same encrypted blob again, and everybody waiting for it downloads that copy.
End-to-end encryption
When you create a collaborative document, your browser generates a random document key and puts it in the URL fragment:
https://collab.word-in-web.com/?doc=...#k=...
Collab WIW uses the document key to create separate AES-GCM keys for document edits, media, and cursor positions. Each kind of data gets its own encryption lane.
The share code adds another layer. You send the link and the code separately. The browser stretches the code, mixes it into key generation, and uses the stretched value to prove that you can join the room.
Your browser keeps the document key and raw share code. The server receives the room ID, a verifier, encrypted envelopes, and the metadata it needs to route them.
Why it works
The final shape lands somewhere between traditional operational transformation and peer-to-peer collaboration:
- The server gives everybody one edit order.
- The browsers hold the durable document.
- Stable IDs make edits point at the same document pieces.
- A small transform adjusts character positions when two people edit the same piece of text.
- Each browser runs a hidden mirror that derives the confirmed document.
- The visible editor adds your unconfirmed typing on top so it still feels immediate.
The backend is light because its job is ordering and relaying encrypted data. The browsers already have the full document engine, so they handle parsing, validation, editing, and page layout.
Super TLDR: The server handles when an edit happened. The client browsers handle what the edit means.
Agents
@wordinweb/agent wraps the WordInWeb DOCX engine in a schema-enforced interface built for AI runtimes. This way agents can create a document from scratch, load DOCX bytes without DOM, and connect to editors and work with human collaborators.
An agent sees the document as paragraphs, runs, tables, comments, equations, drawings, fields, headers, footers, notes, and page geometry. Kind of like the DOCX package but lighter. Every editable thing gets a reference such as block:..., run:..., or object:.... The agent works with these semantic targets, and the package translates to internal XML paths.
The package exposes six portable tools:
composecreates a complete document with paragraphs, tables, equations, charts, images, SmartArt, headers, and footers.capabilitiesreturns the closed JSON schema for one exact operation.inspectreturns document content and stable edit references.editapplies a bounded batch against a specific revision.assetreads image or object bytes when a task needs them.savewrites the resulting DOCX.
Inspection works progressively so context never gets blown out by a big document. The default budget is 100 blocks or 24,000 characters. That response includes the fields needed for common text work. More focused requests expand formatting, bookmarks, tables, objects, assets, or page geometry when the task calls for them.
Basically the agent gets the useful map first, then zooms into the expensive stuff. This saves a wild amount of context compared with serializing the whole DOCX model.
To avoid collisions, every inspection result carries a revision. The package also records a fingerprint for each paragraph, table, and object returned by that inspection. When the room advances, WordInWeb compares the edit target with its inspected fingerprint. An unchanged target proceeds across the newer revision. A changed target returns needs_sync, and the agent reads that section again before it retries. So two agents can keep working in separate sections while the collaboration sequencer handles the intent order.
Every agent operation is treated as an intent used by the editor. A headless document applies the intent atomically to its own DOCX. A collab document sends it through the local or online collab session. This is the important part IMO: the agent interface ends at the same intent layer as a human edit, so all of the existing validation, stable IDs, transform logic, undo data, encryption, and rendering keep working.
How the live BYO Agent demo uses it
The live demo adds a small bootstrap around that package. You bring an existing Codex or Claude Code session and give it a temporary WordInWeb invitation. So BYO Agent but no MCP.
When you click Copy AI link, the browser builds a payload with the room address, document ID, document key, share code, agent name, agent token, private chat key, expiration, and connection instructions. The server stores the ciphertext under a short random ID. The decryption key stays in the URL and reaches the agent through the copied command.
The copied block contains one exact npx command. Codex or Claude passes that string through its normal shell tool, which preserves the full URL mechanically. The command downloads the agent package and starts a detached Node bridge on the agent’s computer.
That bridge decrypts the invitation, opens a WebSocket, and joins the room with an AI participant profile. It uses the same encrypted collaboration client as the browser and connects AgentDocument to the live DocxDocument. The bridge receives the current checkpoint, replays the encrypted intent tail, and builds a current local mirror before the agent edits.
The bridge attaches itself to the current Codex or Claude session. After setup, the model turn ends and the Node process keeps the WebSocket alive with a 25-second ping. The agent starts again when a message arrives.
The inviter’s message arrives at the bridge. The bridge resumes the same Codex or Claude task and supplies the message as the next prompt. Each model action then runs as a short command through a local socket owned by that bridge:
syncgets the latest revision, activity, roster, and agent mode.inspectreads compact context or a focused document detail.capabilitiesgets the schema for the required edit.editsubmits a revision-bound operation batch.chatsends the result back to the inviter.closeends the bridge session.
Edits enter the room as normal editing intents.
Agents start in suggestion mode so you can review changes before they get applied. You can ofc switch it to editing mode. The mode change travels through the private agent channel and updates the bridge immediately.
The inviter can also tag the agent in a document comment. WordInWeb packages the comment, selected text, and target reference into a private task. The agent starts at the correct section and follows the active suggestion or editing mode.
Private chat uses a separate AES-GCM key shared by the inviter and the agent bridge. Other collaborators get the shared parts that matter to collaboration: the AI profile, presence, cursor, comments, suggestions, and edits.
So the full shape is pretty small. The agent package supplies the DOCX tools. The encrypted invitation supplies temporary room access. The Node bridge keeps the collaboration connection alive and wakes the existing model session. The intent system carries the actual work to every browser in order.
P.S. This server is running on an old MacBook Air.