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?
- Build your own adapter: the step-by-step guide.
- Example: the PostgreSQL adapter: a full implementation, annotated.
- Authentication: the hooks that consume
getInfo.