Architecture
Jspreadsheet Server is an authoritative sequencer. For every document (identified by its guid) the server keeps exactly one live, headless spreadsheet instance in memory. Every change, whether it arrives over a WebSocket from a collaborating browser or over the REST API from your backend, is applied to that instance as one ordered operation, then broadcast to every connected peer.
This model keeps all peers consistent by construction. The server never broadcasts its own computed side effects. It re-sends the original operation, and every peer replays it with the same spreadsheet engine, so formulas, cascades and structural shifts are recomputed identically everywhere, from the same input, by the same code.
The life of a change
Step 1: local optimistic apply
A peer applies the change locally (optimistic) and emits the operation, method name and arguments, through the socket. REST API calls skip this step; they start at the server.
Step 2: sequencing
The server sequences it. The operation is applied to the live instance for that guid and the document's revision counter (rev) is incremented in the same synchronous block, so revision order always equals apply order.
Step 3: persistence
The server persists it. Your change handler (or a ready-made adapter) receives the operation, not the whole document, so storage cost is proportional to the change. Persistence does not block the acknowledgement or the broadcast: the handler runs fire-and-forget. If it fails, it reports the failure and the server broadcasts a forceRefresh so clients reload from storage instead of drifting.
Step 4: broadcast
The server broadcasts it. Every other peer in the document's room receives the operation, stamped with its rev, and replays it locally.
Operation flow diagram
browser A ──┐ ┌──▶ browser B (replays op)
│ op: setValue C5=10 │
▼ │
┌─────────────────────────────┐ │
│ live instance for guid │─┘
│ apply → rev++ → persist │──▶ your database (change handler)
└─────────────────────────────┘
One document, one instance, one node
A document's guid is the atomic unit of the system. All peers of a document must reach the same server process, because that process holds the only live instance. This is the basis for scaling (see Scaling), and it means the server does not need distributed locks, an operational transform matrix, or CRDT metadata: a single event loop per document provides the ordering.
The in-memory cache
Lazy instance creation
Instances are created lazily: the first access to a document (socket load or any REST call) builds the live instance from your load handler. Creating a document does not build one; the first load does. From then on, every operation works against warm in-memory state, and REST requests are served from the live instance without calling your load handler again.
Garbage collection and eviction
Cached instances are reclaimed by a garbage collector. Both of its policies only evict documents with zero connected sockets, so an active collaboration is never torn down:
server({
// ...
gc: {
// Sweep cadence and idle threshold, in minutes.
// A document untouched for this long (writes and reads both
// count as activity) is eligible for eviction.
interval: 30,
// Maximum number of cached documents. Beyond this, the
// least-recently-used idle documents are evicted immediately,
// protection against a burst of documents arriving faster
// than the timed sweep. 0 disables the cap.
max: 100,
},
});
Sizing the cache from available memory
Size gc.max from available memory: a cached document holds heap proportional to its serialized size, so measure the footprint of your own representative documents (see Scaling). Evicted documents are rebuilt transparently on their next access, at a cost proportional to the document size.
Revisions
Every applied change bumps the document's monotonic rev.
Where the revision travels
The revision travels on:
- the socket acknowledgement of the operation (
{ status: true, rev }), - every operation broadcast to peers,
- the
loadacknowledgement, and - REST write responses (
{ message: 'Done', rev }).
One class of traffic is exempt: border operations (setBorder, resetBorders) are presence indicators, not document changes. They are relayed to peers without being persisted and do not bump rev; they carry the current revision instead.
How clients use the revision
The revision defines the authoritative order of operations and is available to custom clients that consume the broadcast stream (Real-time sync); the official client verifies on reconnection whether anything happened while it was away using the document's version signature. The counter lives with the cached instance: it restarts when a document is rebuilt, which is safe, because it only ever needs to order operations within one cache lifetime.
What you own
The server owns sequencing, spreadsheet logic and fan-out. You own two things, both delivered as plain async handlers:
Authentication hooks
Authentication. beforeConnect, beforeLoad, beforeChange decide who can connect, read and write. See Authentication.
Persistence handlers
Persistence. load, change, create, destroy, replace move documents in and out of your storage. Ready-made adapters exist for MongoDB, PostgreSQL and Redis.
Keep the handlers cheap
Keep both handlers cheap: they sit on the hot path of every operation. Fetch only the fields you need. An ownership check should read the owner and privacy flags, never the whole document.