Real-time creativity, backed by decentralized trust and resiliency.
By Henry Chou and Chris Ruan
ResCanvas is a decentralized, real-time collaborative drawing platform built on top of ResilientDB. It provides a modern web-based canvas that supports multi-user drawing, room-based collaboration, secure access controls, and persistent on-chain storage of stroke data.
Unlike traditional drawing tools that store artwork on centralized servers, ResCanvas records all drawing activity as immutable transactions in ResilientDB. This ensures verifiable history, censorship resistance, and transparent data integrity while preserving a responsive user experience.
Existing cloud drawing tools offer convenience, but rely entirely on centralized infrastructure. Data can be monitored, modified, or removed, and users have no direct control over how their work is stored or shared. Platforms such as Figma, Google Drawings, and Canva offer collaborative features, but they all depend on trust in a single service operator.
ResCanvas addresses these limitations by combining a familiar drawing interface with decentralized storage. Strokes are committed to a fault-tolerant, verifiable transaction ledger, while Redis and MongoDB maintain fast caches for real-time and historical access. The platform supports both public and private rooms, authenticated collaboration, and optional cryptographic signatures for high-trust environments.
- Multi-user real-time drawing with immediate stroke broadcast.
- Room-based collaboration system with access controls.
- Supports multiple collaboration models:
- Public Rooms: Open access with shared drawing history.
- Private Rooms: Access-limited rooms where strokes may be encrypted.
- Secure Rooms: Require client-side wallet signing (e.g., ResVault). Each stroke includes a verifiable signature.
- Supports multiple collaboration models:
- Persistent storage through ResilientDB, ensuring an immutable edit history.
- Redis caching for low-latency updates and undo/redo operations.
- MongoDB warm cache synchronized with the ledger through the
resilient-python-cachesync bridge service. - Secure server-side authentication and authorization using JWTs.
- Backend enforces all membership and permission checks.
- Optional encrypted strokes and user-level cryptographic signing for secure rooms.
- Responsive React frontend styled with Material Design principles.
ResCanvas is composed of four main layers.
The frontend implements the drawing tools, canvas rendering, session state, and real-time updates via Socket.IO. It performs local smoothing and batching of strokes and attaches JWT access tokens to authenticated requests. It does not enforce security logic; validation occurs in the backend.
The backend forms the trust boundary of the application:
- Validates JWT tokens and enforces room permissions.
- Receives stroke submissions and commits them to ResilientDB using GraphQL.
- Emits live updates through Socket.IO to all participants in a room.
- Manages undo/redo stacks and caching of data in Redis.
- Handles optional encryption and signature verification for private and secure rooms.
ResilientDB is the authoritative ledger. Every stroke is written as a transaction and included in a block after consensus. This provides ordering, immutability, and verifiable authorship for all drawing activity.
A lightweight synchronization service (resilient-python-cache) listens to ResilientDB block streams, parses new transactions, and mirrors them into MongoDB. This provides a fast and queryable history view. Redis stores ephemeral state such as recent strokes and undo/redo queues.
Because ResilientDB is append-only, certain operations, such as undo and redo actions, are represented as semantic overlays rather than destructive modifications. The backend records these actions into the Redis caching layer and, when required, writes reversible markers to the ledger. Clients then replay strokes based on the authoritative ordering in ResilientDB.
This guide provides complete instructions to deploy ResCanvas locally, including setup for the cache layer, backend, and frontend.
Before starting, ensure the following dependencies are installed:
- Python ≥ 3.10 and ≤ 3.12
- Node.js (LTS version via
nvm install --lts) - npm (latest version)
- Redis
- MongoDB Atlas account with a working connection URI
- Go to https://account.mongodb.com/account/login and log in or register.
- Create a project and cluster (if not already existing).
- Within your cluster, click Connect → Drivers and copy the connection string from Step 3
- Keep this MongoDB connection URI for use in later
.envfiles.
git clone https://github.com/ResilientApp/ResCanvas.git
cd ResCanvasCheck your Python installation:
python3 --versionThis cache layer synchronizes strokes between MongoDB and ResilientDB.
cd backend/incubator-resilientdb-resilient-python-cache/
pip install resilient-python-cacheCreate a .env file in this directory with the following content
(replace everything between brackets):
MONGO_URL = "[URI_COPIED_FROM_MONGODB_CONNECTION]"
MONGO_DB = "canvasCache"
MONGO_COLLECTION = "strokes"
python3 example.pyThis starts a MongoDB caching service that syncs data with ResilientDB via the resilientdb://crow.resilientdb.com endpoint defined in cache.py.
The backend handles authentication, REST APIs, and interfaces with ResilientDB.
cd backend/
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtpython gen_keys.pyCopy the printed public and private keys.
Create a new .env under the backend/ folder with the following contents
(replace values between brackets):
MONGO_ATLAS_URI=[URI_COPIED_FROM_MONGODB_CONNECTION]
SIGNER_PUBLIC_KEY=[PUBLIC_KEY_COPIED_FROM_GEN_KEYS_PY]
SIGNER_PRIVATE_KEY=[PRIVATE_KEY_COPIED_FROM_GEN_KEYS_PY]
RESILIENTDB_BASE_URI=https://crow.resilientdb.com
RESILIENTDB_GRAPHQL_URI=https://cloud.resilientdb.com/graphql
Redis is required for caching and backend operations.
brew install redis
brew services start redissudo apt-get update
sudo apt-get install -y redis-server
sudo systemctl restart redis.serviceredis-cli pingExpected output: PONG
If you encounter bcrypt issues, run:
pip install 'passlib>=1.7.4' 'bcrypt>=4.1.2,<5'python app.pyThe frontend provides the ResCanvas web UI.
cd frontend/
nvm install --lts
nvm use --lts
npm i -g npm
npm install
npm startThe app should now be running at http://localhost:[...]
- Create/list rooms: POST/GET
/rooms - Room details: GET
/rooms/<roomId> - Post stroke: POST
/rooms/<roomId>/strokes(requires auth and room access) - Get strokes: GET
/rooms/<roomId>/strokes(works with or without auth but returns membership-scoped data when authenticated) - Undo/redo/clear:
/rooms/<roomId>/undo,/rooms/<roomId>/redo,/rooms/<roomId>/clear
- Login:
POST /auth/login— Validates credentials server-side, returns access token and sets refresh cookie. - Refresh:
POST /auth/refresh— Issues new access token using the refresh cookie. - Logout:
POST /auth/logout— Invalidates refresh token and clears cookie. - Registration:
POST /auth/register— Creates new user account with password hashing. - Current User:
GET /auth/me— Returns authenticated user's profile (requires valid access token).
For secure rooms (type secure) strokes must be signed client-side; the backend validates signatures in submit_room_line.py.
ResCanvas provides a versioned REST API (/api/v1/*) for external applications to integrate collaborative drawing functionality. This generalized API layer allows developers to build third-party apps, mobile clients, integrations, and automation tools on top of ResCanvas.
Canvas API (/api/v1/canvases/*) - Generic, RESTful canvas management
- Decoupled from frontend-specific terminology
- Consolidated endpoint structure (e.g.,
/history/*for undo/redo operations) - Proper HTTP method semantics (DELETE for clearing, not POST)
- Uses
canvasIdparameter for broader applicability - See: API_REFERENCE.md for complete documentation
Consolidated History Operations:
/api/v1/canvases/{canvasId}/history/undo- Undo last action/api/v1/canvases/{canvasId}/history/redo- Redo action/api/v1/canvases/{canvasId}/history/status- Get undo/redo status/api/v1/canvases/{canvasId}/history/reset- Reset historyDELETE /api/v1/canvases/{canvasId}/strokes- Clear canvas (RESTful)
Proper HTTP Method Usage:
- DELETE for clearing strokes
- PATCH for updates, DELETE for removals
All API v1 endpoints are prefixed with /api/v1 as shown below.
Authentication (/api/v1/auth/*):
POST /api/v1/auth/register— Register new userPOST /api/v1/auth/login— Login and obtain JWT tokenPOST /api/v1/auth/refresh— Refresh access tokenPOST /api/v1/auth/logout— Logout and invalidate tokensGET /api/v1/auth/me— Get current user infoPOST /api/v1/auth/change-password— Change password
Canvases (/api/v1/canvases/*):
POST /api/v1/canvases— Create new canvasGET /api/v1/canvases— List accessible canvasesGET /api/v1/canvases/{id}— Get canvas detailsPATCH /api/v1/canvases/{id}— Update canvas settingsDELETE /api/v1/canvases/{id}— Delete canvasPOST /api/v1/canvases/{id}/strokes— Add stroke to canvasGET /api/v1/canvases/{id}/strokes— Get all canvas strokesDELETE /api/v1/canvases/{id}/strokes— Clear canvasPOST /api/v1/canvases/{id}/history/undo— Undo last strokePOST /api/v1/canvases/{id}/history/redo— Redo undone strokeGET /api/v1/canvases/{id}/history/status— Get undo/redo statusPOST /api/v1/canvases/{id}/history/reset— Reset historyPOST /api/v1/canvases/{id}/share— Share canvas with usersGET /api/v1/canvases/{id}/members— Get canvas membersPOST /api/v1/canvases/{id}/leave— Leave shared canvasPOST /api/v1/canvases/{id}/invite— Invite users to canvas
Collaboration (/api/v1/collaborations/*):
GET /api/v1/collaborations/invitations— List pending invitationsPOST /api/v1/collaborations/invitations/{id}/accept— Accept invitationPOST /api/v1/collaborations/invitations/{id}/decline— Decline invitation
Notifications (/api/v1/notifications/*):
GET /api/v1/notifications— List notificationsPOST /api/v1/notifications/{id}/mark-read— Mark as readDELETE /api/v1/notifications/{id}— Delete notificationDELETE /api/v1/notifications— Clear all notificationsGET /api/v1/notifications/preferences— Get preferencesPATCH /api/v1/notifications/preferences— Update preferences
Users (/api/v1/users/*):
GET /api/v1/users/search?q={query}— Search usersGET /api/v1/users/suggest— Get user suggestions
Comprehensive test suites are available as well:
# Backend API v1 tests (Canvas API)
cd backend
pytest tests/test_api_v1_canvases.py -v
# All API v1 tests
pytest tests/test_api_v1*.py -v# Login
TOKEN=$(curl -X POST http://localhost:10010/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"password123"}' \
| jq -r '.token')
# Create canvas
CANVAS_ID=$(curl -X POST http://localhost:10010/api/v1/canvases \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"My Canvas","type":"public"}' \
| jq -r '.room.id')
# Add stroke
curl -X POST http://localhost:10010/api/v1/canvases/$CANVAS_ID/strokes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"stroke": {
"drawingId": "stroke-123",
"color": "#FF0000",
"lineWidth": 3,
"pathData": [{"x":10,"y":20},{"x":30,"y":40}],
"timestamp": 1704067200000,
"user": "alice"
}
}'
# Get strokes
curl -X GET http://localhost:10010/api/v1/canvases/$CANVAS_ID/strokes \
-H "Authorization: Bearer $TOKEN"
# Undo last action
curl -X POST http://localhost:10010/api/v1/canvases/$CANVAS_ID/history/undo \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# Clear canvas
curl -X DELETE http://localhost:10010/api/v1/canvases/$CANVAS_ID/strokes \
-H "Authorization: Bearer $TOKEN"We welcome contributions! The API layer is designed to be extended with new endpoints. Please see CONTRIBUTING.md for guidelines before you start.
See backend/config.py and set the following environment variables as appropriate (examples shown in the repository's .env usage):
MONGO_ATLAS_URI/MONGO_URI— MongoDB connection stringJWT_SECRET— HMAC secret for signing access tokensACCESS_TOKEN_EXPIRES_SECS,REFRESH_TOKEN_EXPIRES_SECS— token lifetimesREFRESH_TOKEN_COOKIE_NAME,REFRESH_TOKEN_COOKIE_SECURE,REFRESH_TOKEN_COOKIE_SAMESITEROOM_MASTER_KEY_B64— used to (re)wrap room keys for private/secure roomsSIGNER_PUBLIC_KEY,SIGNER_PRIVATE_KEY,RECIPIENT_PUBLIC_KEY— used when committing strokes via the GraphQL service
The code loads environment variables via python-dotenv in backend/config.py.
- Login to obtain access token (also sets refresh cookie):
curl -X POST http://127.0.0.1:10010/auth/login -H "Content-Type: application/json" -d '{"username":"testuser","password":"testpass"}' - Post a stroke (replace
<token>and<roomId>):curl -X POST http://127.0.0.1:10010/rooms/<roomId>/strokes -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -d '{"drawingId":"d1","color":"#000","lineWidth":3,"pathData":[],"timestamp": 1696940000000}'
ResCanvas has a comprehensive test suite with tests that are covering both the backend and frontend:
# Fast optimized testing which is recommended for development
./scripts/run_all_tests_parallel.sh --fast
# Full test suite with coverage
./scripts/run_all_tests_parallel.sh
# Tests with sequential script
./scripts/run_all_tests_unified.sh-
Backend Tests (99 tests):
- Unit tests:
pytest tests/unit/ -v - Integration tests:
pytest tests/integration/ -v - E2E tests:
pytest tests/test_*.py -v
- Unit tests:
-
Frontend Unit Tests (139 tests):
- Run with Jest:
cd frontend && npm test - Parallel by default (4 workers)
- Run with Jest:
-
Frontend E2E Tests (56 tests):
- Playwright:
cd frontend && npx playwright test - Tests auth, rooms, collaboration, drawing, error handling
- Playwright:
GitHub Actions workflows automatically test every push and PR:
- Full Test Suite (
ci-tests.yml): Matrix testing across Python 3.10/3.11 and Node 20.x/22.x - Quick Check (
ci-quick.yml): Fast feedback loop for PRs
CI Setup Notes:
- Key Generation: The workflows automatically run
gen_keys.pyto generate signing keys as per the README setup instructions. This requiresPyNaClandbase58packages (now included in requirements.txt). - MongoDB Connection: In GitHub Actions, the MongoDB service is accessible at hostname
mongodb(notlocalhost). The workflows usemongodb://testuser:testpass@mongodb:27017/?authSource=admin. - Environment variable names: The backend expects
JWT_SECRET(notJWT_SECRET_KEY) andRES_DB_BASE_URI/RESILIENTDB_BASE_URIfor ResilientDB endpoint configuration. - Codecov uploads: If the repository is private, set a
CODECOV_TOKENsecret in Settings → Secrets → Actions. The workflows skip Codecov upload if the token is not defined. - Manual trigger: Actions → CI - Full Test Suite → Run workflow
- Henry Chou - Team Leader and Full Stack Developer
- Varun Ringnekar - Full Stack Developer
- Chris Ruan - Frontend Developer
- Shaokang Xie - Backend Developer
- Yubo Bai - Frontend Developer
