Adapters

Overview

The server core never talks to a database. Every persistence decision is delegated to handlers you pass to server({...}), and an adapter is a module that implements those handlers for a specific database, plus the extra methods the extensions and your access-control code rely on. Ready-made adapters exist for MongoDB, PostgreSQL and Redis; all three implement the same persistence contract, so switching storage is a one-line change in your wiring.

The adapter surface

Core contract methods

Two groups of methods. The first is the core contract, driven by the server itself:

Method Called when Notes
load(guid, auth, cachedConfiguration) A document is loaded; warm loads pass the live config as cachedConfiguration Return the config (object or JSON string), or false for not found
change(guid, changes, auth, onerror) After every applied operation, in order The incremental write path. See contract
create(guid, config, auth) A document is created Config arrives parsed and validated
replace(guid, config, auth) Full-document overwrite (snapshots, error resync)
destroy(guid, auth) A document is deleted

Methods for extensions and access control

The second group is not called by the core. It exists for your authorization hooks and the extensions, and you are free to extend it:

Method Consumed by Purpose
getInfo(guid) Your getUserLevel Lightweight access-control projection: owner, privacy flag, invited users, never the document body
get(guid) Your hooks Full document row, for cases that require it
list(auth) GET /api/list The documents owned by the token's subject
getUsers(guid) / setUsers(guid, data) Sharing routes Invited users with their invitation hash and level
listChats / getChat / setChat / deleteChat The agent extension Chat sessions per document and user
setUsage(guid, userId, tokens) / getUsage(guid, userId) The agent extension Accumulated AI token usage counters
getPrompts(guid, userId) / setPrompts(guid, data, userId) The agent extension (legacy) Per-user prompt storage predating chat sessions; used as a fallback when no chat store is configured

Performance rule: projections, not document fetches

Answer access-control questions with projections, not document fetches. getInfo reads three fields; on a multi-megabyte document, fetching the whole body to check ownership significantly reduces throughput (measured).

MongoDB adapter

Storage model and incremental persistence

One documents collection holds one document per spreadsheet (_id = guid, status, user_id, users, created/updated timestamps, and the spreadsheet body); the chats and usage collections carry the agent's data. It uses incremental persistence: every spreadsheet operation is translated to a targeted update, with $set on the exact cell path for a value change and $push/$pull for worksheet and media arrays, so a one-cell edit on a million-cell document writes a few bytes rather than megabytes.

Connection

The adapter connects to mongodb://mongodb, the Docker Compose service name, and uses the jspreadsheet database. The connection target is not configurable through adapter options.

Per-guid write queue

Operations for one document are applied to MongoDB strictly in order; different documents write concurrently.

Error resync

If an incremental update fails, the adapter drains the pending queue and writes the full live config via replace: one expensive write instead of a divergent tail.

16 MB document limit

Keep images out of the document body. Store them on S3 and persist the URL.

Install the MongoDB adapter

npm install @jspreadsheet/server-mongodb

PostgreSQL adapter

JSONB incremental storage

The PostgreSQL adapter stores each spreadsheet in a JSONB column and applies the same incremental model: operations become jsonb_set updates on the exact changed property paths inside the document. It shares the method-translation layer with the MongoDB adapter, so the two have identical coverage of spreadsheet operations, and it manages its own schema (documents, chats and token-usage tables) on first use.

See the dedicated PostgreSQL adapter page for the schema, configuration and internals.

Redis adapter

Snapshot persistence model

The Redis adapter provides basic data persistence: on every change it re-serializes the whole live document (changes.instance.getConfig()) and stores it as one JSON string under the document's guid. This is correct for small documents (Redis values up to 512 MB) but unsuitable as an incremental store for large ones. See the sizing rules.

Method surface and connection

The Redis adapter implements the core contract only: get, load, create, destroy and change. It does not provide replace, getInfo, list or the extension-store methods. It connects to host redis on port 6379 (the Docker Compose service name); the connection target is not configurable through adapter options.

Install the Redis adapter

npm install @jspreadsheet/server-redis

Wiring an adapter

Server wiring example

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

require('dotenv').config();

server({
    config: {
        cors: { origin: "*" },
    },
    port: 3000,
    load: async function(guid, auth, cachedConfiguration) {
        return await adapter.load(guid, auth, cachedConfiguration);
    },
    change: async function(guid, changes, auth, onerror) {
        return await adapter.change(guid, changes, auth, onerror);
    },
    create: async function(guid, config, auth) {
        return await adapter.create(guid, config, auth);
    },
    destroy: async function(guid, auth) {
        return await adapter.destroy(guid, auth);
    },
    replace: async function(guid, config, auth) {
        return await adapter.replace(guid, config, auth);
    },
    list: async function(auth) {
        return await adapter.list(auth);
    },
    error: function(e) {
        console.error('Error', e);
    },
    license: {
        clientId: process.env.JSS_CLIENT,
        licenseKey: process.env.JSS_LICENSE,
    },
});

Wiring extension stores

Extension stores plug into the same adapter, for example the agent's chat and usage persistence (Extensions):

agent({
    prompt: {
        get: async (guid, auth, sessionId) => adapter.getChat(guid, userId(auth), sessionId),
        set: async (guid, messages, auth, sessionId) => adapter.setChat(guid, userId(auth), sessionId, messages),
        list: async (guid, auth) => adapter.listChats(guid, userId(auth)),
        delete: async (guid, auth, sessionId) => adapter.deleteChat(guid, userId(auth), sessionId),
    },
    usage: {
        set: async (guid, usage, auth) => adapter.setUsage(guid, userId(auth), usage),
        get: async (guid, auth) => adapter.getUsage(guid, userId(auth)),
    },
});

Building your own adapter

The upgrade path

Any storage works if you implement the surface above: SQLite, DynamoDB, S3, your existing application database. Start with a snapshot adapter (~60 lines, sufficient for small documents in production), then upgrade the change handler incrementally with debounce, a per-guid write queue, failure resync and operation-level writes. The complete walkthrough with working code for each step is on Build your own adapter, and the formal payload specification is the persistence contract.

Worked example: the PostgreSQL adapter

For a complete, annotated incremental implementation, read the PostgreSQL adapter as a worked example. It reuses the MongoDB translation layer against a different backend, an approach also available to your own adapter if your store can address nested paths.

What's Next?