Server options reference

Complete reference for the options object passed to server({ ... }). Handlers may be async; returned promises are awaited unless noted. Error propagation differs per handler and is noted where it matters.

Core

port

Type: number

Port to listen on. Default 3000. When a server is supplied, the server calls its listen with this port; otherwise Socket.IO listens on it directly.

config

Type: object

Passed to the Socket.IO server constructor, so you can set cors, transports, etc. The server sets maxHttpBufferSize: 2e8 (200 MB) as a default; anything you put in config is merged over it and can override it.

license

Type: { clientId, licenseKey }

Mandatory. Without it the factory returns an Error instead of the server core and never starts listening. The license also gates extension registration: the formula extension and everything in extensions are only registered when a license is present.

extensions

Type: object

Server extensions to register, e.g. { api } for the REST API. Each extension receives the server core object at registration, which gives it access to the cache, the Socket.IO instance and your options.

server

Type: http.Server

Optional HTTP server to attach Socket.IO to. The REST API extension provides one automatically (it assigns its own http.createServer to this option during registration); without one, Socket.IO listens by itself.

error

Type: (e) => void | Promise

Receives internal errors: garbage-collection failures, exceptions thrown inside socket event handlers, and exceptions thrown by your hooks (auth, beforeConnect, connect, disconnect). It is awaited. Without this handler those errors are silently dropped. One exception: a failure while building a document instance inside load is logged to the console and reported to the caller as "Spreadsheet not found".

Cache lifecycle

Eviction never touches a document with connected sockets, regardless of policy.

gc.interval

Type: number (minutes)

Sweep cadence and idle threshold. Documents untouched for this long (reads and writes both refresh the activity timestamp) with zero connected sockets are evicted. Default 30. The first sweep runs one interval after start, and each sweep schedules the next. A value of 0 falls back to the default, because the timed sweep cannot be disabled.

gc.max

Type: number

Maximum cached documents. The limit is checked whenever a new document enters the cache; when exceeded, documents are evicted in order of oldest activity until the count is back within the limit, skipping any document with connected sockets. This covers bursts that arrive faster than the timed sweep. Enforcement is asynchronous and does not delay the load that triggered it. Default 0 (no cap).

Access control hooks

Deny semantics differ by hook: beforeConnect and auth deny on any falsy return value; beforeLoad and beforeChange deny only when the return value is strictly false.

beforeConnect

Signature: (auth)

Called from the Socket.IO connection middleware and once per REST request. On the socket path, auth is the handshake query: everything the client passed in its connection auth, plus type: 'socket' and the client-generated user id; denial rejects the connection with an Invalid authentication error. If the hook throws on the socket path, the error goes to error and the connection is allowed, so deny by returning falsy, not by throwing. On the REST path, auth is { type: 'api', route, token, guid, invitation } (guid and invitation only when present); denial returns 403 Forbidden. Form endpoints are self-authorized and skip this hook.

beforeLoad

Signature: (guid, auth)

Called at the start of every document load, meaning the socket load event and every REST request that addresses a document, before your load handler runs. Keep it cheap (project only access-control fields; never fetch the document body here). On denial, sockets receive { error: 1, message: 'Spreadsheet not found' } and REST returns 404 Not found: denial is deliberately indistinguishable from a missing document.

beforeChange

Signature: (guid, changes, auth)

Called before an operation is applied to the live instance. changes carries the incoming operation: method, args and the target worksheet (see the operation object). Deny to reject the op: sockets receive { status: false, error: 1, message: 'Permission Denied' }, REST receives 403 Forbidden. Two special cases: border operations (setBorder, resetBorders) are presence traffic and bypass this hook on the socket path; the snapshot REST routes call it with { method: 'createSnapshot' }, { method: 'restoreSnapshot', args: [version] } or { method: 'deleteSnapshot', args: [version] } in addition to the isOwner check.

isOwner

Signature: (guid, auth)

Ownership check for privileged REST routes: snapshot listing, read, create, restore and delete, and document rename. Extension routes (for example invited-user management) use it too. Return true only for the document owner; anything else yields 403 Forbidden. If the option is not defined, these routes skip the ownership gate entirely.

auth

Signature: (query)

Legacy socket-only pre-check, runs before beforeConnect in the same middleware with the same handshake query and the same semantics (falsy denies; a thrown error is reported and the connection proceeds). Prefer beforeConnect.

connect

Signature: (query, user)

Notification after a socket connects, not a gate. user is the user value from the handshake query, or a generated UUID when absent. Exceptions are routed to error.

disconnect

Signature: (query, user)

Notification after a socket disconnects. Only fires for sockets that connected with a guid (i.e. joined a document room). Before it runs, the server broadcasts a resetBorders operation for each worksheet so the remaining peers drop the departed user's selection borders.

Persistence handlers

The server is storage-agnostic; these five handlers are the entire contract. Exact payload shapes are specified in the persistence contract.

load

Signature: (guid, auth, cachedConfiguration)

Return the document configuration (an object or a JSON string), or false if it does not exist. When the document is already cached and the full configuration is needed (a socket load), cachedConfiguration carries the live config so you can return it and skip your database read. REST requests against a cached document do not call this handler at all; they use the live instance directly. The first successful load builds the in-memory instance; if construction fails, the error is logged to the console and the document is reported as not found. The snapshot routes also call this handler (with two arguments) to serialize the current document.

change

Signature: (guid, changes, auth, onerror)

Persist one applied operation. changes is { instance, worksheet, method, args }, where instance is the live server-side spreadsheet and worksheet is the target worksheetId. The call is fire-and-forget: the acknowledgement and broadcast do not wait for storage. On failure call onerror(), and the server broadcasts a forceRefresh ("There were an error trying to save the last operation.") so clients reload rather than drift. Do not rely on throwing: a rejected promise from this handler is not caught by the server.

create

Signature: (guid, config, auth)

Store a new document. Callers submit config as a JSON string; the server parses and normalizes it before your handler runs: worksheets must be a non-empty array, every worksheet must have a worksheetName, and worksheetId, namespace and version are filled with generated UUIDs when missing. Creation does not build the in-memory instance; the first load does. This path is gated only by the connection check (beforeConnect), not by beforeChange.

destroy

Signature: (guid, auth)

Delete the document. On a truthy return, or when the handler is not defined at all, the server evicts the cache, broadcasts a forceRefresh ("This spreadsheet has been deleted") and disconnects every socket in the room. On a falsy return, nothing is evicted.

replace

Signature: (guid, config, auth)

Overwrite the whole document. Called only by snapshot restore, with the decoded snapshot configuration as an object; the server snapshots the current version first. If absent, the server falls back to calling your destroy and create handlers directly, and that fallback bypasses the create-time normalization and does not disconnect the room.

list

Signature: (auth)

Return the documents visible to this user. The core server never calls it; it serves extension features, namely the document-list REST route and the MCP document-list tool.

destroyCache

Signature: (guid, config)

Optional notification when a cached instance is evicted. Only when this is defined does the server serialize the final config to pass here, so leave it undefined to avoid that serialization cost. It is also called with config = null when the evicted guid was not in local memory, so adapter-side caches (e.g. Redis) can clean up as well. The call is not awaited.

Minimal complete example

const server = require('@jspreadsheet/server');
const adapter = require('@jspreadsheet/server-mongodb');

server({
    port: 3000,
    license: { clientId: process.env.JSS_CLIENT, licenseKey: process.env.JSS_LICENSE },
    config: { cors: { origin: '*' } },
    gc: { interval: 30, max: 100 },

    beforeConnect: async (auth) => true,
    beforeLoad: async (guid, auth) => canRead(auth, guid),
    beforeChange: async (guid, changes, auth) => canEdit(auth, guid),
    isOwner: async (guid, auth) => isDocumentOwner(auth, guid),

    load: (guid, auth) => adapter.load(guid, auth),
    change: (guid, changes, auth, onerror) => adapter.change(guid, changes, auth, onerror),
    create: (guid, config, auth) => adapter.create(guid, config, auth),
    destroy: (guid, auth) => adapter.destroy(guid, auth),
    replace: (guid, config, auth) => adapter.replace(guid, config, auth),
    list: (auth) => adapter.list(auth),

    error: (e) => console.error(e),
});