Ship the Collaborative backend in 10 minutes
Real-time relay, comments, suggestions, version history, and async save for Froala's Collaborative plugin. Copy one Node app, deploy to the host you already use, and paste the URL into your editor.
Four modules, one HTTP server
wysiwyg-editor-node-sdk. The WebSocket relay and all REST endpoints share one port and one collab.db SQLite file. Collaborative
/collab/{docId} CollabPersistence
/collab/{docId}/versions VersionControl
/collab/{docId}/content AsyncSave
01. Create the backend
const path = require('path'); const fs = require('fs'); const http = require('http'); const express = require('express'); const cors = require('cors'); const FroalaEditor = require('wysiwyg-editor-node-sdk'); const { Collaborative, CollabPersistence, VersionControl, AsyncSave } = FroalaEditor; const PORT = parseInt(process.env.PORT, 10) || 3000; // DATA_DIR must point at a persistent volume on your host (see step 2). const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data'); const DB_PATH = path.join(DATA_DIR, 'collab.db'); const CORS_ORIGINS = (process.env.CORS_ORIGINS || '*').trim(); fs.mkdirSync(DATA_DIR, { recursive: true }); const app = express(); app.set('trust proxy', true); app.use(cors(CORS_ORIGINS === '*' ? { origin: true } : { origin: CORS_ORIGINS.split(',').map(o => o.trim()).filter(Boolean) })); app.use(express.json({ limit: '5mb' })); // All REST routes are namespaced under /collab/:docId by the SDK. CollabPersistence.attachRoutes(app, { dbPath: DB_PATH }); VersionControl.attachRoutes(app, { dbPath: DB_PATH }); AsyncSave.attachRoutes(app, { dbPath: DB_PATH }); // Health check — reports live relay stats for your host's probe. app.get('/health', (_req, res) => res.json({ status: 'ok', ...Collaborative.getStats() })); // One HTTP server carries both Express (REST) and the WebSocket relay. const server = http.createServer(app); Collaborative.attachToServer(server); server.listen(PORT, () => console.log('Collaborative backend on :' + PORT));
{
"name": "froala-collaborative-backend",
"version": "1.0.0",
"private": true,
"main": "server.js",
"engines": { "node": ">=20" },
"scripts": { "start": "node server.js" },
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"wysiwyg-editor-node-sdk": "^5.3.0"
}
}
# Port — most hosts inject this automatically; leave unset in production. PORT=3000 # Directory for collab.db. MUST be a persistent volume on your host. DATA_DIR=./data # Comma-separated origins your editor page is served from ("*" for a first test). CORS_ORIGINS=*
02. Deploy to your host
{
"$schema": "https://railway.com/railway.schema.json",
"build": { "builder": "NIXPACKS", "buildCommand": "npm install" },
"deploy": {
"startCommand": "npm start",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE",
"numReplicas": 1
}
}
New project from your repo
On railway.com: New Project → Deploy from GitHub repo. In Settings → Source, set Root Directory to your backend folder and point the config path at
railway.json.Attach a volume
Settings → Volumes → New Volume, mount path
/data. The volume keeps your data through every redeploy.Set variables
DATA_DIR=/dataandCORS_ORIGINS. LeavePORTunset, since Railway injects it.Deploy & get the URL
Settings → Networking → Generate Domain. That domain is your backend.
services: - type: web name: froala-collaborative runtime: node rootDir: backend plan: starter # a disk requires a paid instance type buildCommand: npm install startCommand: npm start healthCheckPath: /health numInstances: 1 # single writer — do not raise envVars: - key: DATA_DIR value: /var/data - key: CORS_ORIGINS value: "*" disk: name: collab-data mountPath: /var/data sizeGB: 1
New Blueprint Instance
On render.com: New → Blueprint, connect your repo. Render reads
render.yamland shows the service with its disk.Confirm the disk
Verify a disk named
collab-dataat/var/datais listed. A paid instance type is required, since it mounts the disk and stays awake between requests.Apply
Render builds and deploys. Adjust
CORS_ORIGINSin the dashboard when you go past testing.Grab the URL
Open
https://froala-collaborative-XXXX.onrender.com.
app = "froala-collaborative" primary_region = "sea" [env] DATA_DIR = "/data" PORT = "3000" CORS_ORIGINS = "*" [http_service] internal_port = 3000 force_https = true auto_stop_machines = false # keep the relay warm auto_start_machines = false min_machines_running = 1 # single instance only [[http_service.checks]] method = "GET" path = "/health" interval = "30s" timeout = "5s" [[mounts]] source = "collab_data" destination = "/data" [[vm]] size = "shared-cpu-1x" memory = "512mb"
FROM node:20-slim WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ RUN npm install --omit=dev COPY . ./ ENV NODE_ENV=production DATA_DIR=/data PORT=3000 EXPOSE 3000 CMD ["npm", "start"]
brew install flyctl # or: curl -L https://fly.io/install.sh | sh fly auth signup fly launch --no-deploy # accept app name + region fly volumes create collab_data --size 1 fly deploy fly open
Install flyctl & sign in
Run the first two commands above.
Launch, then make the volume
--no-deployfirst so you can createcollab_databefore the first boot.Deploy
fly deploybuilds the image and boots one machine with the volume at/data.Open it
Your URL is
https://<app-name>.fly.dev.
03. Point your editor at it
wss:// scheme (all three hosts serve HTTPS). docId is any string that names a shared document. new FroalaEditor('#editor', { docId: 'my-doc-2024', realTimeConfig: { // your deployed backend, plus the docId syncUrl: 'wss://your-app.up.railway.app/' + 'my-doc-2024' } });
Where the REST endpoints live
https://your-app.up.railway.app/collab/{docId}/… on the same domain as the relay. One service, one URL to configure. Three things that apply to every host
Keep WebSockets open
SQLite needs a volume
DATA_DIR. With it, comments, suggestions, and version history survive every redeploy.