License: Apache-2.0
A universal JavaScript client library for building collaborative drawing applications with blockchain-backed storage. This toolkit provides a complete SDK for managing canvases, submitting drawing strokes, real-time collaboration, and integrating with ResilientDB's decentralized infrastructure.
- 🎨 Drawing Operations: Submit strokes, undo/redo, clear canvas
- 🔐 Authentication: JWT-based auth with secure canvas support
- 🏠 Canvas Management: Create, share, and manage collaborative drawing canvases
- ⚡ Real-Time Collaboration: Socket.IO-based live updates
- 🔗 ResilientDB Integration: Immutable stroke storage on blockchain
- 🛡️ Secure Canvases: Cryptographic signing for verified contributions
- 📦 Modular Design: Use only what you need
- 🔄 Auto-Retry Logic: Built-in request retry with exponential backoff
drawing-toolkit/
├── src/ # Source code
│ ├── index.js # Main client export
│ └── modules/ # SDK modules (auth, canvases, socket, etc.)
├── examples/ # Example applications
├── tests/ # Test suite
├── dist/ # Built files (generated)
├── LICENSE # Apache 2.0 License
├── README.md # This file
└── package.json # Package configuration
npm install collaborative-drawing-toolkitOr using yarn:
yarn add collaborative-drawing-toolkitThis toolkit is a client library and requires a compatible backend server to function.
You must have the following services running and accessible:
- Backend API Server - Handles authentication, authorization, and data management
- ResilientDB - Decentralized blockchain database for immutable storage
- MongoDB - Warm cache and queryable data replica
- Redis - In-memory cache for real-time performance
This toolkit is designed to work with any backend that implements the required API endpoints. For a reference implementation, see compatible backend servers that support:
- JWT authentication (
/api/v1/auth/*) - Canvas management (
/api/v1/canvases/*) - Real-time collaboration via Socket.IO
- ResilientDB integration for blockchain storage
Example backend setup:
# Install backend dependencies
pip install flask flask-socketio redis pymongo resilientdb
# Configure environment variables
export MONGO_URI="mongodb://localhost:27017"
export REDIS_URL="redis://localhost:6379"
export RESILIENTDB_ENDPOINT="http://localhost:8000"
# Start backend server
python app.py
# Backend runs on http://localhost:10010 by defaultimport DrawingClient from 'collaborative-drawing-toolkit';
// Initialize the client (point to your backend server)
const client = new DrawingClient({
baseUrl: 'http://localhost:10010', // Your backend server URL
apiVersion: 'v1'
});
// Authenticate
const { token, user } = await client.auth.login({
username: 'alice',
password: 'securePassword'
});
// Create a collaborative drawing canvas
const { canvas } = await client.canvases.create({
name: 'My Artwork',
type: 'public' // 'public', 'private', or 'secure'
});
// Submit a drawing stroke
await client.canvases.addStroke(canvas.id, {
pathData: [
{ x: 100, y: 100 },
{ x: 200, y: 150 },
{ x: 300, y: 200 }
],
color: '#FF0000',
lineWidth: 3
});
// Real-time collaboration
client.socket.connect(token);
client.socket.joinCanvas(canvas.id);
client.socket.on('new_line', (stroke) => {
console.log('New stroke from collaborator:', stroke);
// Render the stroke on your canvas
});This toolkit integrates with a backend server and ResilientDB infrastructure to provide:
- Immutable Storage: All drawing strokes are stored in ResilientDB via GraphQL
- Caching Layer: Redis for real-time performance, MongoDB for queryable replica
- Real-Time Updates: Socket.IO for instant collaboration
- Authentication: JWT-based with wallet support for secure operations
const client = new DrawingClient({
baseUrl: 'https://api.example.com', // Required: Your backend server URL
apiVersion: 'v1', // Optional: API version (default: 'v1')
timeout: 30000, // Optional: Request timeout ms (default: 30000)
retries: 3, // Optional: Retry attempts (default: 3)
onTokenExpired: async () => { // Optional: Token refresh handler
// Return new token or throw error
return await refreshToken();
}
});const { token, user } = await client.auth.register({
username: 'alice',
password: 'securePassword123',
walletPubKey: 'optional_wallet_public_key' // For secure canvases
});const { token, user } = await client.auth.login({
username: 'alice',
password: 'securePassword123'
});await client.auth.logout();const { user } = await client.auth.getMe();const { canvas } = await client.canvases.create({
name: 'Collaborative Canvas',
type: 'public', // 'public', 'private', or 'secure'
description: 'Optional description'
});const { canvases, pagination } = await client.canvases.list({
sortBy: 'createdAt',
order: 'desc',
page: 1,
per_page: 20
});const { canvas } = await client.canvases.get(canvasId);const { canvas } = await client.canvases.update(canvasId, {
name: 'New Name',
description: 'Updated description',
archived: false
});await client.canvases.delete(canvasId);await client.canvases.share(canvasId, [
{ username: 'bob', role: 'editor' },
{ username: 'carol', role: 'viewer' }
]);const { strokes } = await client.canvases.getStrokes(canvasId, {
since: timestamp, // Optional: get strokes after this time
until: timestamp // Optional: get strokes before this time
});await client.canvases.addStroke(canvasId, {
pathData: [
{ x: 10, y: 20 },
{ x: 30, y: 40 },
{ x: 50, y: 60 }
],
color: '#000000',
lineWidth: 2,
tool: 'pen', // Optional: tool name
signature: '...', // Optional: for secure canvases
signerPubKey: '...' // Optional: for secure canvases
});await client.canvases.undo(canvasId);await client.canvases.redo(canvasId);await client.canvases.clear(canvasId);const { invites } = await client.invites.list();await client.invites.accept(inviteId);await client.invites.decline(inviteId);const { notifications } = await client.notifications.list();await client.notifications.markRead(notificationId);await client.notifications.delete(notificationId);await client.notifications.clear();const { preferences } = await client.notifications.getPreferences();await client.notifications.updatePreferences({
canvasInvites: true,
mentions: true,
canvasActivity: false
});// Connect to Socket.IO server
client.socket.connect(token);
// Join a room for real-time updates
client.socket.joinCanvas(canvasId);
// Listen for events
client.socket.on('new_line', (stroke) => {
// New stroke added by collaborator
});
client.socket.on('undo_line', (data) => {
// Stroke was undone
});
client.socket.on('redo_line', (data) => {
// Stroke was redone
});
client.socket.on('clear_canvas', () => {
// Canvas was cleared
});
client.socket.on('user_joined', (user) => {
// User joined the room
});
client.socket.on('user_left', (user) => {
// User left the room
});
// Leave room
client.socket.leaveCanvas(canvasId);
// Disconnect
client.socket.disconnect();This toolkit is designed to work seamlessly with ResilientDB's blockchain infrastructure:
A compatible backend server should integrate:
- ResilientDB KV Service: For blockchain storage
- GraphQL Service: For transaction submission
- Redis: For real-time caching
- MongoDB: For queryable stroke replica
- Sync Service: To mirror ResilientDB data to MongoDB
Example backend structure:
backend/
├── app.py # Server application
├── routes/ # API endpoints
├── services/
│ ├── graphql_service.py # ResilientDB GraphQL client
│ └── db.py # Redis + MongoDB setup
└── middleware/
└── auth.py # JWT authentication
- Client → Submits stroke via SDK
- Backend → Writes to ResilientDB (immutable)
- Backend → Caches in Redis (fast reads)
- Backend → Broadcasts via Socket.IO (real-time)
- Sync Service → Mirrors to MongoDB (queryable)
See examples/basic-drawing-app.html for a complete example.
import DrawingClient from 'collaborative-drawing-toolkit';
class DrawingApp {
constructor(canvasElement) {
this.canvas = canvasElement;
this.ctx = this.canvas.getContext('2d');
this.client = new DrawingClient({
baseUrl: 'http://localhost:10010'
});
this.currentCanvas = null;
this.isDrawing = false;
this.currentPath = [];
}
async init() {
// Login
const { token } = await this.client.auth.login({
username: 'artist',
password: 'password'
});
// Create or join canvas
const { canvas } = await this.client.canvases.create({
name: 'My Canvas',
type: 'public'
});
this.currentCanvas = canvas;
// Setup real-time collaboration
this.client.socket.connect(token);
this.client.socket.joinCanvas(canvas.id);
this.client.socket.on('new_line', (stroke) => {
this.drawStroke(stroke);
});
// Setup canvas events
this.setupCanvasEvents();
}
setupCanvasEvents() {
this.canvas.addEventListener('mousedown', (e) => {
this.isDrawing = true;
this.currentPath = [{ x: e.offsetX, y: e.offsetY }];
});
this.canvas.addEventListener('mousemove', (e) => {
if (!this.isDrawing) return;
this.currentPath.push({ x: e.offsetX, y: e.offsetY });
this.drawLocalPath(this.currentPath);
});
this.canvas.addEventListener('mouseup', async (e) => {
if (!this.isDrawing) return;
this.isDrawing = false;
// Submit to server
await this.client.canvases.addStroke(this.currentRoom.id, {
pathData: this.currentPath,
color: '#000000',
lineWidth: 2
});
});
}
drawStroke(stroke) {
const { pathData, color, lineWidth } = stroke;
this.ctx.strokeStyle = color;
this.ctx.lineWidth = lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(pathData[0].x, pathData[0].y);
pathData.slice(1).forEach(point => {
this.ctx.lineTo(point.x, point.y);
});
this.ctx.stroke();
}
drawLocalPath(pathData) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawStroke({ pathData, color: '#000000', lineWidth: 2 });
}
}
// Usage
const canvas = document.getElementById('drawingCanvas');
const app = new DrawingApp(canvas);
app.init();import DrawingClient from 'collaborative-drawing-toolkit';
import { signMessage } from './wallet-utils';
const client = new DrawingClient({
baseUrl: 'http://localhost:10010'
});
// Register with wallet
await client.auth.register({
username: 'secure_user',
password: 'password',
walletPubKey: myWalletPublicKey
});
// Create secure canvas
const { canvas } = await client.canvases.create({
name: 'Secure Collaborative Canvas',
type: 'secure'
});
// Submit signed stroke
const strokeData = {
pathData: [{ x: 10, y: 20 }, { x: 30, y: 40 }],
color: '#FF0000',
lineWidth: 3
};
const signature = await signMessage(JSON.stringify(strokeData), myWalletPrivateKey);
await client.canvases.addStroke(canvas.id, {
...strokeData,
signature: signature,
signerPubKey: myWalletPublicKey
});try {
await client.canvases.create({ name: 'Test', type: 'public' });
} catch (error) {
if (error.isAuthError()) {
// 401: Token expired or invalid
console.error('Please log in again');
} else if (error.isValidationError()) {
// 400: Invalid input
const errors = error.getValidationErrors();
console.error('Validation errors:', errors);
} else if (error.status === 403) {
// Forbidden
console.error('Insufficient permissions');
} else {
// Other errors
console.error('Error:', error.getUserMessage());
}
}When deploying a backend server for this toolkit:
# MongoDB for queryable stroke replica
MONGO_ATLAS_URI=mongodb+srv://...
# ResilientDB credentials
SIGNER_PUBLIC_KEY=...
SIGNER_PRIVATE_KEY=...
RESILIENTDB_BASE_URI=https://crow.resilientdb.com
RESILIENTDB_GRAPHQL_URI=https://cloud.resilientdb.com/graphql
# Redis for real-time caching
REDIS_HOST=localhost
REDIS_PORT=6379npm testContributions are welcome! Please follow standard open-source contribution practices.
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Open a Pull Request
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.