Deploy a collaboration server to enable real-time document synchronization using our Yjs-based collaboration library.
Quick Start
Install the library
npm install @harbour-enterprises/superdoc-yjs-collaboration
Create server
import SuperDocCollaboration from '@harbour-enterprises/superdoc-yjs-collaboration';
const collab = new SuperDocCollaboration()
.withDebounce(2000)
.onAuthenticate(async ({ token }) => {
const user = await validateToken(token);
return user;
})
.onLoad(async ({ documentId }) => {
return await loadDocument(documentId);
})
.onAutoSave(async ({ document, documentId }) => {
await saveDocument(documentId, document);
})
.build();
Add WebSocket endpoint
app.ws('/collaboration/:documentId', (ws, req) => {
collab.welcome(ws, req);
});
Required Hooks
onAuthenticate
Validate user access to documents.
Authentication token from client
Original HTTP request with headers
Returns: User object or throws error
.onAuthenticate(async ({ token, documentId, request }) => {
// Option 1: JWT validation
const payload = jwt.verify(token, SECRET);
// Option 2: Session cookie
const session = parseCookie(request.headers.cookie);
// Verify permissions
const canAccess = await checkPermissions(userId, documentId);
if (!canAccess) {
throw new Error('Access denied');
}
return {
userId: payload.sub,
name: payload.name,
email: payload.email
};
})
Always validate permissions - throwing an error prevents access
onLoad
Load document state from storage.
Returns: Uint8Array | null
- Document state or null for new
.onLoad(async ({ documentId }) => {
const doc = await storage.get(documentId);
if (!doc) {
// New document
return null;
}
// Must return Uint8Array
return new Uint8Array(doc);
})
onAutoSave
Save document state (debounced).
import * as Y from 'yjs';
.onAutoSave(async ({ document, documentId }) => {
// Convert to Uint8Array for storage
const state = Y.encodeStateAsUpdate(document);
await storage.save(documentId, state);
// Optional: Track metadata
await db.updateMetadata(documentId, {
lastModified: new Date(),
size: state.byteLength
});
})
Optional Hooks
onChange
Fires on every change - use sparingly!
.onChange(({ documentId }) => {
// Light operations only
metrics.increment('edits');
updateLastActive(documentId);
})
Configure Yjs document on creation.
.onConfigure(({ ydoc }) => {
// Disable garbage collection
ydoc.gc = false;
// Add custom types
ydoc.getMap('metadata');
})
Configuration
Builder Methods
Set service identifier.withName('collab-prod-1')
Autosave interval (milliseconds).withDebounce(2000) // Save every 2 seconds
Cache expiry when no users connected.withDocumentExpiryMs(300000) // 5 minutes
Storage Implementations
import { Pool } from 'pg';
const pool = new Pool();
const storage = {
async get(documentId) {
const { rows } = await pool.query(
'SELECT data FROM documents WHERE id = $1',
[documentId]
);
return rows[0]?.data || null;
},
async save(documentId, data) {
await pool.query(
`INSERT INTO documents (id, data, updated_at)
VALUES ($1, $2, NOW())
ON CONFLICT (id)
DO UPDATE SET data = $2, updated_at = NOW()`,
[documentId, Buffer.from(data)]
);
}
};
Framework Integration
import express from 'express';
import expressWs from 'express-ws';
const app = express();
expressWs(app);
const collab = new SuperDocCollaboration()
// ... hooks
.build();
app.ws('/collaboration/:documentId', (ws, req) => {
collab.welcome(ws, req);
});
app.listen(3000);
Production Deployment
Environment Variables
# .env
NODE_ENV=production
WS_PORT=3000
JWT_SECRET=your-secret-key
REDIS_URL=redis://localhost:6379
DATABASE_URL=postgres://user:pass@localhost/db
Docker Setup
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Scaling Considerations
For production scale:
- Use Redis for document state (fast access)
- Implement sticky sessions for WebSocket
- Add health checks for load balancer
- Monitor memory usage per document
- Set connection limits per user
Security Checklist
Always use WSS in production
url: 'wss://collab.example.com' // Not ws://
Implement rate limiting
const rateLimit = new Map();
.onAuthenticate(async ({ token, request }) => {
const ip = request.ip;
if (rateLimit.get(ip) > 100) {
throw new Error('Rate limit exceeded');
}
// ... rest of auth
})
Validate document IDs
.onLoad(async ({ documentId }) => {
// Prevent path traversal
if (!/^[a-zA-Z0-9-]+$/.test(documentId)) {
throw new Error('Invalid document ID');
}
// ... load document
})
Set connection limits
const connections = new Map();
.onAuthenticate(async ({ token }) => {
const user = validateToken(token);
const userConnections = connections.get(user.id) || 0;
if (userConnections >= 5) {
throw new Error('Connection limit reached');
}
connections.set(user.id, userConnections + 1);
return user;
})
Monitoring
Health Check Endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
documents: collab.getActiveDocumentCount(),
connections: collab.getConnectionCount(),
uptime: process.uptime()
});
});
Metrics
// Track key metrics
.onChange(() => {
metrics.increment('document.edits');
})
.onAutoSave(() => {
metrics.increment('document.saves');
})
.onAuthenticate(() => {
metrics.increment('auth.attempts');
})
Examples
API Reference
Full builder API documentation:
Fluent builder for configuration