Collaborative Review Workflows in Froala Without a WebSocket Server
Posted on By Mostafa Yousef | In Tutorials,
You don’t need a WebSocket server to get practical collaboration in Froala. If your app runs on serverless platforms, shared hosting, or any infrastructure that kills idle sockets, Froala’s asynchronous mode gives you comments, track changes and version history without the pain of running a WebSocket relay.
It isn’t real-time typing: collaborators work in turns, and changes appear after a save and refresh. For most editorial workflows, where one person edits and others review, that trade-off is a net win: reviewers work at their own pace, and nothing depends on everyone being online at once.
Here’s exactly what you get:
| Feature | Async mode |
|---|---|
| Comments and replies | ✅ Fully supported |
| Suggestions (track changes) | ✅ Fully supported |
| Version history | ✅ Fully supported |
| Live cursors | ❌ Not available |
| Instant sync while typing | ❌ Not available |
For review workflows, which is what most teams actually want, this trade is often a better fit. Reviewers work at their own pace, and nobody needs to be online at the same time.
Key Takeaways
- No WebSocket needed. Leaving out
realTime.syncUrlswitches Froala into async mode, and three REST modules from the Node SDK handle everything else. - Review features work fully. Comments, replies, suggestions and version history all persist to a single SQLite file.
- Collaborators take turns. There are no live cursors, and updates appear after the other person saves and you refresh.
- The document follows last save wins. Comments and suggestions never conflict, so the simplest pattern is one editor and any number of suggesters.
- You can upgrade later. Adding real-time sync reuses the same persistence routes, so nothing you build here is thrown away.
What We’ll Build
We’ll build a small page where one person writes a draft and another person reviews it with comments and suggestions. Everything is saved, so each person sees the other’s work after refreshing. The backend is three REST modules and about 20 lines of code.
Prerequisites
- Node.js 20 or newer (22 is recommended). Check by running
node -vin your terminal. - A code editor such as VS Code.
- A terminal, like the one built into VS Code (View → Terminal).
That’s it. No database to install; SQLite is just a file on disk.
Step 1: Create the Project
Open your terminal and run these commands one at a time:
mkdir froala-async-review cd froala-async-review npm init -y npm install express wysiwyg-editor-node-sdk
The first two commands create a project folder and move into it. npm init -y creates a package.json file, which is your project’s ID card. The last line installs Express, which is our web server, and Froala’s Node SDK, which includes the collaboration modules. SQLite support (better-sqlite3) comes with the SDK automatically.
Step 2: Create the Server
Create a file called server.js in your project folder. We’ll build it in three small pieces.
Piece 1: Load the tools.
const path = require('path'); // Builds file paths that work on every OS
const fs = require('fs'); // Lets us create folders
const express = require('express'); // Our web server framework
// Grab the three collaboration modules we need from Froala's SDK
const { CollabPersistence, VersionControl, AsyncSave } = require('wysiwyg-editor-node-sdk'); The code loads the libraries we installed. Notice that Collaborative, the WebSocket module, is missing from the list. We don’t need it.
Piece 2: Set up Express.
const app = express();
// Understand JSON requests (used for comments, suggestions and versions)
app.use(express.json({ limit: '5mb' }));
// Understand form-encoded requests (used by Froala's save plugin)
app.use(express.urlencoded({ extended: true, limit: '5mb' }));
// Serve our HTML page from the "public" folder
app.use(express.static(path.join(__dirname, 'public'))); This is where many developers get stuck, so here’s why both parsers matter. Comments and suggestions arrive as JSON, but Froala’s save plugin sends the document as a form. Without express.urlencoded(), every save fails with a quiet 400 error and your document never persists.
Piece 3: Attach the collaboration routes and start the server.
// SQLite needs its folder to exist before it can create the database file
const dataDir = path.join(__dirname, 'data');
fs.mkdirSync(dataDir, { recursive: true });
const dbPath = path.join(dataDir, 'collab.db');
// All three modules share one database file
CollabPersistence.attachRoutes(app, { dbPath }); // Comments and suggestions
VersionControl.attachRoutes(app, { dbPath }); // Version history
AsyncSave.attachRoutes(app, { dbPath }); // The document content itself
app.listen(3000, () => {
console.log('Server running at http://localhost:3000');
}); Each attachRoutes call adds a set of REST endpoints to your server. Together they create:
/collab/:docId/commentsfor comment cards and replies/collab/:docId/suggestionsfor track-change suggestions/collab/:docId/versionsfor saved snapshots/collab/:docId/contentfor the document’s HTML
The :docId part is the room number where collaborators meet. Everyone using the same docId works on the same document.
Step 3: Create the Page
Create a folder called public, and inside it, a file called index.html.
Piece 1: The HTML shell.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Froala Async Review</title>
<!-- Froala's packaged CSS, which includes the collaboration plugin styles -->
<link href="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/css/froala_editor.pkgd.min.css" rel="stylesheet">
</head>
<body>
<!-- Buttons for saving the document and creating a version -->
<button id="save">Save document</button>
<button id="version">Save a version</button>
<span id="status"></span>
<!-- The editor will appear inside this div -->
<div id="editor"></div>
<!-- Froala's packaged JS, which includes the collaboration plugin -->
<script src="https://cdn.jsdelivr.net/npm/froala-editor@5.4.0/js/froala_editor.pkgd.min.js"></script>
<script>
// Our code goes here (next steps)
</script>
</body>
</html> The packaged build includes every plugin, collaboration included, so two files are all you need.
Piece 2: Decide who’s using the page. Paste this inside the empty <script> tag:
// Read the user's name and role from the URL, for example ?name=Sam&role=suggester
const params = new URLSearchParams(location.search);
const userName = params.get('name') || 'Guest';
const userRole = params.get('role') || 'editor'; // editor, suggester or viewer
const userId = userName.toLowerCase().replace(/\s+/g, '-');
const docId = 'review-doc'; // The room number
const base = '/collab/' + docId; // Shared start of every URL
// Small helper that shows messages next to the buttons
function setStatus(text) {
document.getElementById('status').textContent = text;
} Reading identity from the URL lets you test two users in two tabs without building a login system. This is for learning only; a real app gets identity from its login session.
Piece 3: Load the saved document, then start the editor.async function start() {
// Ask the server for the last saved version of the document
const res = await fetch(base + '/content');
if (res.ok) {
const saved = await res.json();
// Put the saved HTML into the div BEFORE the editor starts
document.getElementById('editor').innerHTML = saved.content;
}
// A 404 is fine: it just means nobody has saved this document yet
const editor = new FroalaEditor('#editor', {
// Where the save plugin sends the document
saveURL: base + '/content',
// Extra fields sent with every save, so we know who saved
saveParams: { authorId: userId, authorName: userName },
// Autosave 5 seconds after changes (you'll also have a manual button)
saveInterval: 5000,
collabConfig: {
docId: docId,
user: { id: userId, name: userName, role: userRole },
commentsUrl: base + '/comments',
suggestionsUrl: base + '/suggestions',
versionControl: { url: base + '/versions' }
// No realTime.syncUrl here. Leaving it out is what turns on async mode.
},
events: {
// Runs after a successful save
'save.after': function () {
setStatus('Saved at ' + new Date().toLocaleTimeString());
},
// Runs if the save fails
'save.error': function () {
setStatus('Save failed. Is the server running?');
}
}
});
// Manual save button: sends the document right now
document.getElementById('save').addEventListener('click', () => editor.save.save());
// Version button: stores a named snapshot in version history
document.getElementById('version').addEventListener('click', () => {
editor.collaborative.saveVersion('Snapshot by ' + userName, null)
.then(() => setStatus('Version saved'));
});
}
start(); Two important ideas here.
First, the missing syncUrl is the whole switch. Without a WebSocket address, Froala stores comments, suggestions and versions through your REST URLs instead of broadcasting them live.
Second, the order matters. Comment and suggestion cards come from the comments and suggestions tables, but the highlights in the text live inside the document HTML. That’s why we load the saved content before starting the editor: the highlights and the cards find each other on startup. It’s also why you must save the document after adding a comment, or the card will exist with no highlighted text to point to.
We wrap everything in an async function because await at the top level of a regular script is a syntax error in the browser.
Step 4: Run It
Back in your terminal, from the project folder:
node server.js You should see Server running at http://localhost:3000. Leave this terminal open. Closing it stops the server.
⚠️ Open the page through the server, not by double-clicking the HTML file. A file:// page can’t reach your /collab routes.
Step 5: Test a Review Workflow
Open two browser tabs:
- Tab A (the writer):
http://localhost:3000/?name=Alex&role=editor - Tab B (the reviewer):
http://localhost:3000/?name=Sam&role=suggester
Now walk through a real review:
- In Tab A, type a paragraph and click Save document.
- Refresh Tab B. Sam sees Alex’s paragraph. 🎉
- In Tab B, Sam starts in Suggesting mode because of the suggester role. Change a word, then select a sentence and add a comment. Click Save document.
- Refresh Tab A. Alex sees Sam’s comment and suggestion in the side panel.
- Alex accepts the suggestion, replies to the comment, and clicks Save document.
- Click Save a version to capture the reviewed draft.
Congratulations! You just built a complete review workflow with a server that has no WebSocket at all. Stop the server, start it again, and refresh: everything is still there, because it lives in data/collab.db.
The One Rule of Async Mode
Since there’s no live sync, the document itself follows last save wins. If Alex and Sam both edit the same paragraph at the same time, the later save replaces the earlier one.
Comments and suggestions are safe, because each one is its own database row. The document text is the only thing to coordinate. The easiest pattern: one person owns the text as the editor, and everyone else reviews as a suggester. That’s how most editorial teams already work.
Troubleshooting
Saves fail with a 400 error. Check that express.urlencoded() is in server.js. The save plugin sends form data, not JSON.
The page loads but nothing saves. Make sure you opened http://localhost:3000, not the file directly, and that the terminal running node server.js is still open.
Comments show in the panel but the text isn’t highlighted. The comment was stored, but the document wasn’t saved afterward. Click Save document after reviewing.
EADDRINUSE when starting the server. Something else is using port 3000, often an old copy of your server. Close the other terminal or change 3000 to 3001 in server.js.
npm install fails while building better-sqlite3. Your Node version is probably too old. Upgrade to Node 22 and reinstall.
I don’t see Sam’s changes. In async mode, updates appear when you refresh, after the other person has saved.
Before Going to Production
This demo is intentionally simple. Before real users touch it:
- Add authentication. Put your own login check in front of the routes with
app.use('/collab', requireLogin)before theattachRoutescalls, and check that each user may access that docId. - Replace URL identity. Take name and role from the user’s session, never from the query string.
- Sanitize HTML on the server. The content endpoint stores HTML exactly as received, and our page inserts it with
innerHTML. Clean it with a library such assanitize-htmlbefore saving.
Frequently Asked Questions
Does my backend have to be Node.js?
No. The editor only talks to plain REST endpoints, so any language can serve them. The Node SDK is the quickest route, but a PHP, Rails or Django app can implement the same routes by following the request and response shapes in the backend API reference.
Will this run on a serverless platform as written?
The routes will, but the SQLite file won’t survive. Serverless functions usually get a temporary disk that’s wiped between runs, so your data would disappear. On serverless, keep the same endpoints and store the data in a hosted database instead.
Can I see other people’s changes without refreshing the page?
Partly. Calling editor.collaborative.syncCollabData() re-fetches comments and suggestions, and editor.collaborative.checkForUpdates() refreshes the version list. You could run these on a button or a timer. The highlighted text lives in the document itself, though, so a refresh is still the most reliable way to see everything a reviewer did.
Do I have to click Save every time?
Not usually. The save plugin autosaves a few seconds after you stop typing, based on saveInterval. The manual button is a safety net, and it’s handy right after accepting suggestions or adding comments.
Can versions be created automatically?
Yes. Add autoSaveEnabled: true inside versionControl, and Froala saves a snapshot every 60 seconds by default. Use autoSaveInterval to change the timing, in milliseconds.
What happens if two people edit the text at the same time?
The later save replaces the earlier one. Comments and suggestions are stored separately and are never overwritten this way. If simultaneous editing is common for your team, that’s the signal to add the real-time relay.
Where to Go Next
When you’re ready for live cursors, the collaboration backend tutorial adds the WebSocket relay on top of the same persistence routes you built today. Your comments, suggestions and versions carry over unchanged. For every option and endpoint, see the collaboration plugin docs and the backend API reference, or try it live in the collaborative editing demo.
- Whats on this page hide
No comment yet, add your voice below!