Server options reference

Options accepted by 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 (cors, transports, etc.). The server defaults maxHttpBufferSize to 2e8 (200 MB); config is merged over the defaults 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, with 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 it, those errors are silently dropped. 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. Checked whenever a document enters the cache; when exceeded, documents are evicted oldest activity first until the count is within the limit, skipping any with connected sockets. This covers bursts faster than the timed sweep. Enforcement is asynchronous and does not delay the triggering load. 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 (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 the document body. On denial, sockets receive { error: 1, message: 'Spreadsheet not found' } and REST returns 404 Not found, 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 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 joined a document room (connected with a guid). Before it runs, the server broadcasts a resetBorders operation for each worksheet so remaining peers drop the departed user's selection borders.

Persistence handlers

The server is storage-agnostic: load, change, create, destroy and replace are the contract, and list and destroyCache are optional. 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 cached and the full configuration is needed (a socket load), cachedConfiguration carries the live config so you can return it and skip the database read. REST requests against a cached document use the live instance and do not call this handler. 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(): 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: the server does not catch a rejected promise from this handler.

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. Not called by the core; it serves the document-list REST route and the MCP document-list tool.

destroyCache

Signature: (guid, config)

Optional notification when a cached instance is evicted. The server serializes the final config to pass here only when this is defined; leave it undefined to avoid that 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. 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),
});