Days
Hours
Minutes
Seconds
x
Skip to content
Froala Documentation

Node.js · Express · SQLite · WebSocket

Collaborative Backend API Reference

Four independent modules power Froala’s collaboration plugin: a real-time WebSocket relay, plus REST + SQLite stores for suggestions & comments, version snapshots, and offline saves. Mount only what you need — or run all four on a single HTTP server and a single database file.

System at a glance
Browser peer Froala + Yjs Browser peer Froala + Yjs Browser peer Froala + Yjs SINGLE HTTP SERVER · PORT 3000 01 Collaborative WebSocket relay · broadcast by docId 02 CollabPersistence suggestions · comments 03 Version Control snapshots 04 AsyncSave offline content upsert collab.db SQLite

Overview

The collaborative backend is split across four modules that you compose as needed: a WebSocket relay for real-time peer sync, REST endpoints for persisting suggestions and comments, a version history store, and an async content-save endpoint for offline mode. All four can share a single HTTP server and a single SQLite file.

ModuleFileResponsibility
Collaborativelib/collaborative.jsPure WebSocket relay. Broadcasts messages between peers of the same document.
CollabPersistencelib/collab_persistence.jsREST + SQLite for track-change suggestions and inline comments.
VersionControllib/version_control.jsREST + SQLite for named or auto snapshots of document content.
AsyncSavelib/async_save.jsREST + SQLite for saving/loading the latest content in offline (async) mode.

Getting started

Install

bash
npm install wysiwyg-editor-node-sdk
SQLite dependency CollabPersistence, VersionControl, and AsyncSave require better-sqlite3. It ships as a regular dependency and installs automatically — no separate step needed.

Import the modules

All collaborative modules are exported from the package’s main entry point. Destructure the ones you need:

javascript · commonjs
const FroalaEditor = require('wysiwyg-editor-node-sdk');

const Collaborative     = FroalaEditor.Collaborative;
const CollabPersistence = FroalaEditor.CollabPersistence;
const VersionControl    = FroalaEditor.VersionControl;
const AsyncSave         = FroalaEditor.AsyncSave;

Conventions

Every REST endpoint is namespaced under /collab/:docId/. The :docId segment identifies the collaborative document and must match the docId used by the WebSocket connection. Document IDs are arbitrary strings; URL-encode any characters outside a-z 0-9 - _.

TransportURL pattern
WSws://<host>/{docId}
RESThttp://<host>/collab/{docId}/{resource}

Full server example

The most common setup runs the relay and all three REST stores on one Express-wrapped HTTP server, backed by one shared SQLite file. This wires everything together end to end:

javascript · server.js
const http    = require('http');
const express = require('express');
const path    = require('path');
const {
  Collaborative,
  CollabPersistence,
  VersionControl,
  AsyncSave,
} = require('wysiwyg-editor-node-sdk');

const app = express();
app.use(express.json());

// One SQLite file shared by all three persistence modules
const dbPath = path.join(__dirname, 'data', 'collab.db');
CollabPersistence.attachRoutes(app, { dbPath });
VersionControl.attachRoutes(app, { dbPath });
AsyncSave.attachRoutes(app, { dbPath });

// Optional: expose live relay stats on the shared server
app.get('/health', (req, res) => res.json(Collaborative.getStats()));

// Share ONE HTTP server between REST and the WebSocket relay
const server = http.createServer(app);
Collaborative.attachToServer(server);

server.listen(3000, () => {
  console.log('Collaborative backend on http://localhost:3000');
});
Why attachToServer? Use attachToServer() (not createServer()) whenever REST endpoints and the relay must live on the same port. createServer() is for running the relay as the only service on a port.

Module 01 — Collaborative

lib/collaborative.js · WebSocket relay

A pure WebSocket relay server. It groups connections by docId and broadcasts every incoming message to all other members of that document. All collaboration logic lives in the browser clients — the server never parses or validates message content. A keepalive heartbeat pings every 30 s and terminates any client that fails to pong within 10 s.

FN Collaborative.createServer(options?)

Creates a self-contained HTTP + WebSocket server. The HTTP side answers GET /health and rejects all other HTTP requests with 426 Upgrade Required. Use it when the relay is the only service on the port.

ParameterTypeDescription
optionsobjectoptionalConfiguration object.
options.portnumberoptionalPort to listen on. Default: 3000.

ReturnsWebSocket.Server, the underlying ws instance.

javascript
const { Collaborative } = require('wysiwyg-editor-node-sdk');

// Starts on port 3000; clients connect to ws://localhost:3000/{docId}
const wss = Collaborative.createServer({ port: 3000 });
FN Collaborative.attachToServer(httpServer)

Attaches the relay to an existing HTTP (or Express-wrapped) server. Use it when REST endpoints and the relay must share one port.

ParameterTypeDescription
httpServerhttp.ServerrequiredA Node.js http.Server instance (including those created by Express).

ReturnsWebSocket.Server.

javascript · express + shared port
const http    = require('http');
const express = require('express');
const { Collaborative } = require('wysiwyg-editor-node-sdk');

const app    = express();
const server = http.createServer(app);
Collaborative.attachToServer(server);
server.listen(3000);
FN Collaborative.setupWSConnection(conn, req)

Low-level handler that wires a single connection into the document registry. Called automatically by both createServer and attachToServer — use it directly only when managing your own ws server.

ParameterTypeDescription
connWebSocketrequiredThe incoming connection from the connection event.
reqhttp.IncomingMessagerequiredThe HTTP upgrade request. The document ID is derived from req.url: the leading slash is removed and the path before the first ? is URL-decoded. Falls back to default-doc if the path is empty.

Returnsvoid.

Document lifecycle A document entry is created when the first client joins and deleted when the last client disconnects. Clients that join an empty document receive no historical state — they must wait for another peer to reconnect and push a sync message.
FN Collaborative.getStats()

Returns a snapshot of server state: active document count and total open connections.

returns
{ "docs": 3, "clients": 7 }
GET /health

Live document and client counts. Available on the standalone server from createServer(). With attachToServer(), add this route yourself using Collaborative.getStats().

200 OK
json
{ "docs": 3, "clients": 7 }
WS ws://<host>/<docId>

Connect to a collaborative document. The path after the leading slash is the URL-decoded document ID. Every text or binary message from one client is relayed verbatim to all other clients in the same document.

SegmentTypeDescription
docIdstringrequiredIdentifies the document. All clients on the same docId are peers. URL-encoded; decoded by the server before grouping. An empty path defaults to default-doc.
Relay semantics The server is a transparent relay: it does not store messages or replay history to late joiners. All CRDT/OT state, cursor sync, and protocol versioning are the client’s responsibility — Froala’s collaborative plugin uses Yjs internally.
Message size limit Any message larger than 1 MB is silently dropped and never forwarded to peers. Keep individual sync messages well below this threshold.
javascript · browser client
// Connect to document "my-doc-2024"
const ws = new WebSocket('ws://localhost:3000/my-doc-2024');

ws.addEventListener('message', (event) => {
  // Handle incoming sync messages from other peers
  handleCollabMessage(event.data);
});

// Send a message — relayed to all other peers in the document
ws.send(encodeUpdate(myUpdate));

Module 02 — CollabPersistence

lib/collab_persistence.js · REST + SQLite

REST endpoints backed by SQLite for persisting track-change suggestions and inline comments per document. Anchor positions are stored as opaque JSON strings — the server never interprets editor-internal position data.

FN CollabPersistence.attachRoutes(app, options?)

Registers all suggestion and comment routes on an Express app and initialises the SQLite tables on first call.

ParameterTypeDescription
appExpressrequiredAn Express application instance.
options.dbPathstringoptionalPath to the SQLite file. Default: collab.db (relative to the process CWD). Created if it doesn’t exist.
javascript
const path = require('path');
const { CollabPersistence } = require('wysiwyg-editor-node-sdk');

CollabPersistence.attachRoutes(app, {
  dbPath: path.join(__dirname, 'data', 'collab.db')
});

Suggestions

Track-change suggestions created by collaborators. Each records the original and proposed text, the editor position (anchor), the author, and a lifecycle status.

GET /collab/:docId/suggestions

Returns all suggestions for a document, ordered by timestamp ascending. The replies field is not included in the list response.

Path paramTypeDescription
docIdstringrequiredDocument identifier.
200 OK — array of suggestion objects

Suggestion object schema

json
{
  "id": "sug-001",
  "doc_id": "demo-doc",
  "type": "insert",          // operation type (opaque to server)
  "author_id": "user-42",
  "author_name": "Alice",
  "timestamp": 1720000000000,   // Unix milliseconds
  "original_text": "Hello world",
  "suggested_text": "Hello everyone",
  "anchor_start": "[0,5]",       // JSON-serialised position array
  "anchor_end": "[0,16]",        // JSON-serialised position array
  "status": "pending"            // "pending" | "accepted" | "rejected"
}
Anchors anchor_start and anchor_end are stored and returned as JSON strings. Run JSON.parse if you need the underlying position array.
POST /collab/:docId/suggestions

Creates a new suggestion. A 409 should be treated as confirmation the suggestion was already persisted.

FieldTypeDescription
idstringrequiredUnique identifier, generated by the client.
typestringrequiredOperation type (e.g. insert, delete, format). Opaque to the server.
authorIdstringoptionalStored as author_id.
authorNamestringoptionalStored as author_name.
timestampnumberoptionalUnix ms. Defaults to server time (Date.now()).
originalTextstringoptionalStored as original_text.
suggestedTextstringoptionalStored as suggested_text.
anchorobjectoptionalPosition object with start and end. Each is JSON-serialised into anchor_start / anchor_end.
statusstringoptionalOne of pending, accepted, rejected. Default: pending.
201 Created 400 missing id or type 409 id already exists
json · request body
{
  "id": "sug-001",
  "type": "insert",
  "authorId": "user-42",
  "authorName": "Alice",
  "originalText": "Hello world",
  "suggestedText": "Hello everyone",
  "anchor": { "start": [0, 5], "end": [0, 16] }
}
json · 201 response
{ "id": "sug-001" }
PATCH /collab/:docId/suggestions/:id

Updates the status and/or replies of a suggestion. At least one field must be present.

FieldTypeDescription
statusstringoptionalOne of pending, accepted, rejected. An invalid value returns 400.
repliesarrayoptionalFull replacement array of reply objects, JSON-serialised before storage. Pass [] to clear.
200 OK 400 empty body / invalid status
json · response variants
// when status is updated
{ "id": "sug-001", "status": "accepted" }

// when only replies is updated
{ "id": "sug-001" }
DELETE /collab/:docId/suggestions/:id

Permanently deletes a suggestion. Returns 204 whether or not the suggestion existed (idempotent).

204 No Content

Comments

Inline comments anchored to a position in the document. Comments support a resolved flag and a replies list.

GET /collab/:docId/comments

Returns all comments for a document, ordered by timestamp ascending. The replies field is not included in the list response.

200 OK — array of comment objects
json · comment schema
{
  "id": "cmt-001",
  "doc_id": "demo-doc",
  "author_id": "user-42",
  "author_name": "Alice",
  "timestamp": 1720000000000,
  "text": "Should we rephrase this paragraph?",
  "anchor_start": "[1,0]",   // JSON-serialised position array
  "anchor_end": "[1,42]",
  "resolved": 0              // 0 = unresolved, 1 = resolved (integer)
}
Type note resolved is returned as an integer (0 or 1), not a boolean. anchor_start / anchor_end are JSON strings — parse them as needed.
POST /collab/:docId/comments

Creates a new comment. A 409 confirms the comment was already persisted.

FieldTypeDescription
idstringrequiredUnique identifier, generated by the client.
authorIdstringoptionalAuthor’s unique identifier.
authorNamestringoptionalAuthor’s display name.
timestampnumberoptionalUnix ms. Defaults to server time.
textstringoptionalComment body. Default: "".
anchorobjectoptionalPosition with start and end. Each is JSON-serialised.
resolvedbooleanoptionalDefault: false. Stored as integer 0/1.
repliesarrayoptionalInitial reply objects. Default: []. JSON-serialised before storage.
201 Created 400 missing id 409 id already exists
json · 201 response
{ "id": "cmt-001" }
PATCH /collab/:docId/comments/:id

Updates the resolved state and/or replies. At least one field must be present.

FieldTypeDescription
resolvedbooleanoptionalTruthy values stored as 1, falsy as 0.
repliesarrayoptionalFull replacement array. Pass [] to clear.
200 OK 400 neither field present 404 comment not found
json · 200 response
{ "id": "cmt-001" }
DELETE /collab/:docId/comments/:id

Permanently deletes a comment.

204 No Content

Module 03 — VersionControl

lib/version_control.js · REST + SQLite

REST endpoints backed by SQLite for storing named or auto-generated snapshots of editor content per document. Each snapshot captures the full document state at a point in time and is optionally annotated with a title and description. Versions are append-only — they cannot be edited in content, only renamed.

FN VersionControl.attachRoutes(app, options?)

Registers version-control routes on an Express app and initialises the versions table on first call.

ParameterTypeDescription
appExpressrequiredAn Express application instance.
options.dbPathstringoptionalSQLite file path. Default: collab.db. Can share the same file as the other modules.
GET /collab/:docId/versions

Returns all version summaries for a document, ordered by created_at descending (newest first). The snapshot field is excluded for performance — use the single-version endpoint to retrieve it.

200 OK — array of version summaries
json · version summary (no snapshot)
{
  "id": "rv-lp8k3a2-9xf2r",
  "doc_id": "demo-doc",
  "title": "Before major refactor",   // null if unnamed
  "description": null,
  "created_at": 1720000000000,
  "author_id": "user-42",
  "author_name": "Alice",
  "is_named": 1,                     // 1 if title is set, 0 otherwise
  "source": "manual"                 // "manual" | "auto"
}
GET /collab/:docId/versions/:id

Returns a single version including the full snapshot field.

200 OK — full version object 404 Not Found
json · full version
{
  "id": "rv-lp8k3a2-9xf2r",
  "doc_id": "demo-doc",
  "title": "Before major refactor",
  "description": null,
  "snapshot": "<p>Document content here</p>",
  "created_at": 1720000000000,
  "author_id": "user-42",
  "author_name": "Alice",
  "is_named": 1,
  "source": "manual"
}
POST /collab/:docId/versions

Creates a new snapshot. If id is omitted, the server generates one as rv-{base36timestamp}-{random}.

FieldTypeDescription
snapshotstringrequiredFull document content (HTML string for Froala).
idstringoptionalClient-supplied identifier. Auto-generated if absent.
titlestringoptionalHuman-readable name. When non-empty, sets is_named = 1.
descriptionstringoptionalLonger description or change notes.
authorobjectoptionalAuthor info. author.idauthor_id, author.nameauthor_name.
sourcestringoptionalauto for periodic auto-saves; anything else stored as manual. Default: manual.
Server-controlled timestamp The server always records the current time as created_at. Any createdAt field in the request body is ignored.
201 Created — { id } only 400 missing snapshot 409 id already exists
json · request body
{
  "snapshot": "<p>Document content here</p>",
  "title": "Before major refactor",
  "author": { "id": "user-42", "name": "Alice" },
  "source": "manual"
}
PATCH /collab/:docId/versions/:id

Updates the title and/or description. Snapshot content cannot be changed. At least one field is required.

FieldTypeDescription
titlestring | nulloptionalNew title. Pass null to clear (sets is_named = 0). A non-empty string sets is_named = 1.
descriptionstring | nulloptionalNew description. Pass null to clear.
200 OK — updated full version 400 neither field present 404 Not Found
DELETE /collab/:docId/versions/:id

Permanently deletes a version snapshot.

204 No Content

Module 04 — AsyncSave

lib/async_save.js · REST + SQLite

REST endpoints for saving and loading the latest editor content per document. Used when the editor runs in async (offline) mode — i.e. when realTimeConfig.syncUrl is not set. Each document has exactly one content row; every POST upserts the current state.

FN AsyncSave.attachRoutes(app, options?)

Registers the async-save routes on an Express app and initialises the async_content table on first call.

ParameterTypeDescription
appExpressrequiredAn Express application instance.
options.dbPathstringoptionalSQLite file path. Default: collab.db. Can share the same file as the other modules.
POST /collab/:docId/content

Saves (upserts) the current editor HTML. Each document has exactly one content row — repeated calls replace the previous value.

FieldTypeDescription
bodystringrequiredFull editor HTML to persist. Field name matches Froala’s default saveParam.
authorIdstringoptionalIdentifier of the user who performed the save.
authorNamestringoptionalDisplay name of the user who performed the save.
Field name The Froala save plugin sends its payload under the field name body (its default saveParam).
200 OK 400 missing body field
json · request body
{
  "body": "<p>Updated document content.</p>",
  "authorId": "user-42",
  "authorName": "Alice"
}
json · 200 response
{ "doc_id": "demo-doc", "saved_at": 1720000000000 }
GET /collab/:docId/content

Returns the most recently saved content for a document.

Sanitization Content is returned verbatim. Sanitizing the HTML before rendering is the caller’s responsibility.
200 OK 404 no content saved
json · 200 response
{
  "doc_id": "demo-doc",
  "content": "<p>Updated document content.</p>",
  "author_id": "user-42",
  "author_name": "Alice",
  "saved_at": 1720000000000
}
json · 404 response
{ "error": "no content saved for this document" }

Error format

All endpoints return errors as JSON with a single error field describing what went wrong. No envelope wrapping, no nested objects.

json · error response
{ "error": "id and type are required" }
StatusWhen
400A required field is missing or a value is invalid (e.g. an invalid status string, or an empty body on a PATCH endpoint).
404The requested resource does not exist.
409A resource with the supplied id already exists (POST suggestions, comments, versions).
500An unexpected database or server error. The error field contains the underlying SQLite message.

Still Have Questions?

Here are some additional resources if you can’t find an answer you need.