Skip to content

Commit ca4e7dc

Browse files
Anant KumarAnant Kumar
authored andcommitted
feat: rebrand to rustCanvas, fix dark mode, add prod deploy
Dark toggle no longer panics on RefCell double-borrow; theme persists in localStorage and applies html.dark for full UI + canvas. Production Dockerfile builds WASM+server together; README deploy guide added.
1 parent 47cc432 commit ca4e7dc

15 files changed

Lines changed: 220 additions & 88 deletions

File tree

.dockerignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
target
2+
dist
3+
.git
4+
.github
5+
*.md
6+
chat_history.md
7+
.env
8+
.env.*
9+
.DS_Store
10+
.idea
11+
.vscode
12+
**/.trunk

README.md

Lines changed: 89 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
# excaildraw
1+
# rustCanvas
22

3-
A scalable, Excalidraw-like collaborative drawing app built in full-stack Rust.
3+
A scalable, Excalidraw-like collaborative whiteboard built in full-stack Rust.
44

5-
- **Frontend:** Yew + WASM + HTML5 Canvas (Trunk) on port **3000**
5+
- **Frontend:** Yew + WASM + HTML5 Canvas (Trunk) — dev on port **3000**
66
- **Backend:** Axum + Tokio + WebSockets on port **8080**
7+
- **Production:** Single server serves WASM UI + API from one port
78
- **Shared core:** Excalidraw-compatible elements, JSON I/O, reconciliation, undo/redo, SVG export, rough sketch paths
8-
- **Persistence:** PostgreSQL (optional via `DATABASE_URL`) with in-memory fallback
9-
- **Scaling:** Redis pub/sub (optional via `REDIS_URL`) for multi-instance WebSocket broadcast
9+
- **Persistence:** PostgreSQL (optional) with in-memory fallback
10+
- **Scaling:** Redis pub/sub (optional) for multi-instance WebSocket broadcast
1011
- **Desktop:** Tauri 2 shell in `apps/desktop`
1112

1213
## Repository
@@ -15,17 +16,15 @@ https://github.com/antcybersec/excaildraw
1516

1617
## Features
1718

18-
- **Drawing tools:** select/move (multi-select with Shift/Cmd), rectangle, ellipse, line, arrow, freehand, text, image
19-
- **Canvas:** infinite pan/zoom, grid, rough/sketch stroke rendering, selection highlights
19+
- **Drawing tools:** select/move, rectangle, ellipse, line, arrow, freehand, text, image, eraser
20+
- **Canvas:** infinite pan/zoom, light/dark theme, rough/sketch strokes, selection highlights
2021
- **Layers panel:** reorder elements, select from list
2122
- **History:** undo/redo (Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y)
2223
- **Import/export:** `.excalidraw` JSON, SVG, PNG
2324
- **Collaboration:** WebSocket rooms, live sync, remote cursors
24-
- **E2E encryption:** optional room password — client encrypts sync payloads (AES-GCM); server relays ciphertext only
25-
- **Offline:** IndexedDB scene cache + pending update queue when disconnected
26-
- **Auth:** optional JWT (`POST /api/auth/token`); set `REQUIRE_AUTH=1` to gate room creation
27-
- **Rate limiting:** in-memory per-IP limits on REST endpoints
28-
- **API:** REST room create/load/save + WebSocket `/ws/{roomId}`
25+
- **E2E encryption:** optional room password (AES-GCM)
26+
- **Offline:** IndexedDB scene cache + pending update queue
27+
- **Auth:** optional JWT; set `REQUIRE_AUTH=1` to gate room creation
2928

3029
## Project structure
3130

@@ -36,16 +35,16 @@ crates/
3635
server/ REST API, WebSocket sync, PostgreSQL, JWT, Redis broker
3736
apps/
3837
desktop/ Tauri 2 native shell
39-
docker/ Postgres + Redis compose + server Dockerfile
38+
docker/ Postgres + Redis compose + production Dockerfile
39+
scripts/ build-release.sh for local production builds
4040
```
4141

4242
## Prerequisites
4343

4444
- Rust 1.75+
4545
- `wasm32-unknown-unknown` target
4646
- [Trunk](https://trunkrs.dev/)
47-
- Docker (optional, for PostgreSQL + Redis)
48-
- Node/npm (optional, only if building Tauri icons via `tauri icon`)
47+
- Docker (optional, for PostgreSQL + Redis + production deploy)
4948

5049
```bash
5150
rustup target add wasm32-unknown-unknown
@@ -56,97 +55,119 @@ cargo install trunk
5655

5756
| Variable | Default | Description |
5857
|----------|---------|-------------|
59-
| `DATABASE_URL` | *(none)* | PostgreSQL connection string; omit for in-memory rooms |
60-
| `REDIS_URL` | *(none)* | Redis URL for cross-instance room broadcast |
58+
| `PORT` | `8080` | HTTP listen port |
59+
| `FRONTEND_DIST` | `dist` | Path to Trunk `dist/` for serving the WASM UI |
60+
| `DATABASE_URL` | *(none)* | PostgreSQL connection string |
61+
| `REDIS_URL` | *(none)* | Redis URL for cross-instance broadcast |
6162
| `JWT_SECRET` | dev secret | HMAC secret for JWT signing |
6263
| `REQUIRE_AUTH` | *(off)* | Set to `1` to require Bearer token on `POST /api/rooms` |
6364

6465
## Development
6566

66-
### 1. Start PostgreSQL + Redis (optional)
67+
### 1. Run API server
6768

6869
```bash
69-
docker compose -f docker/docker-compose.yml up -d
70-
export DATABASE_URL=postgres://excaildraw:excaildraw@localhost:5432/excaildraw
71-
export REDIS_URL=redis://localhost:6379
70+
cargo run -p excaildraw-server
71+
```
72+
73+
### 2. Run web client (separate terminal)
74+
75+
```bash
76+
cd crates/client && trunk serve --open
7277
```
7378

74-
Without `DATABASE_URL`, rooms persist in memory only for the server process lifetime.
79+
Open http://127.0.0.1:3000
80+
81+
> Port **8080** is API-only in dev. The drawing UI is on **3000**.
7582
76-
### 2. Run the API server
83+
### 3. Optional: Postgres + Redis
7784

7885
```bash
79-
cargo run -p excaildraw-server
86+
docker compose -f docker/docker-compose.yml up -d postgres redis
87+
export DATABASE_URL=postgres://excaildraw:excaildraw@localhost:5432/excaildraw
88+
export REDIS_URL=redis://localhost:6379
8089
```
8190

82-
Endpoints:
91+
### Collaboration
8392

84-
| Method | Path | Description |
85-
|--------|------|-------------|
86-
| GET | `/health` | Health check |
87-
| POST | `/api/auth/token` | Issue JWT (`{"username":"..."}`) |
88-
| POST | `/api/rooms` | Create room |
89-
| GET | `/api/rooms/{id}` | Load room |
90-
| PUT | `/api/rooms/{id}` | Save room |
91-
| WS | `/ws/{id}` | Real-time sync |
93+
1. Optionally set a **room password** (E2E encryption)
94+
2. Click **Live collab** — creates a room and updates URL (`?room=...`)
95+
3. Share the URL (and password if used)
96+
4. Draw together — live sync + remote cursors
9297

93-
### 3. Run the web client
98+
### Keyboard shortcuts
99+
100+
| Key | Tool |
101+
|-----|------|
102+
| 1–9 | Tools (eraser = E) |
103+
| Scroll | Zoom |
104+
| Space + drag | Pan |
105+
| Shift | Constrain shapes |
106+
| Ctrl/Cmd + Z | Undo |
107+
108+
## Production deployment
109+
110+
### Option A — Docker (recommended)
111+
112+
Builds WASM client + server into one image. Serves UI and API on port **8080**.
94113

95114
```bash
96-
cd crates/client && trunk serve --open
115+
docker compose -f docker/docker-compose.yml up -d --build
97116
```
98117

99-
Open http://127.0.0.1:3000
118+
Open http://localhost:8080
100119

101-
### 4. Desktop app (Tauri)
120+
Includes Postgres + Redis for persistent rooms and horizontal scaling.
102121

103-
Build the WASM client first, then run Tauri from the desktop crate:
122+
### Option B — Bare metal / VPS
104123

105124
```bash
106-
cd crates/client && trunk build --release
107-
cd ../../apps/desktop/src-tauri && cargo tauri dev
125+
./scripts/build-release.sh
126+
FRONTEND_DIST=./dist ./target/release/excaildraw-server
108127
```
109128

110-
Tauri loads the client from `http://127.0.0.1:3000` in dev or from `dist/` after `trunk build`.
129+
Put **nginx** or **Caddy** in front for HTTPS:
130+
131+
```nginx
132+
server {
133+
listen 443 ssl;
134+
server_name your-domain.com;
135+
136+
location / {
137+
proxy_pass http://127.0.0.1:8080;
138+
proxy_http_version 1.1;
139+
proxy_set_header Upgrade $http_upgrade;
140+
proxy_set_header Connection "upgrade";
141+
proxy_set_header Host $host;
142+
}
143+
}
144+
```
111145

112-
### Collaboration workflow
146+
WebSocket collab requires the `Upgrade` headers above.
113147

114-
1. Optionally enter a **room password** for end-to-end encrypted sync
115-
2. Click **New Room** — creates a room and updates the URL (`?room=...`)
116-
3. Share the URL (and password, if used) with others
117-
4. Draw together — changes sync via WebSocket with reconciliation
118-
5. Remote cursors appear with colored labels
119-
6. Scene auto-saves to IndexedDB for offline reload
148+
### Option C — Static hosting + separate API
120149

121-
### Keyboard shortcuts
150+
1. `cd crates/client && trunk build --release` → upload `dist/` to Cloudflare Pages / Netlify / S3
151+
2. Deploy server to Fly.io / Railway / Render with `DATABASE_URL` + `REDIS_URL`
152+
3. Set client API base via same-origin reverse proxy or configure CORS on server
122153

123-
| Shortcut | Action |
124-
|----------|--------|
125-
| Scroll | Zoom |
126-
| Shift + drag | Pan |
127-
| Shift/Cmd + click | Add to selection |
128-
| Ctrl/Cmd + Z | Undo |
129-
| Ctrl/Cmd + Shift + Z | Redo |
130-
| Ctrl/Cmd + Y | Redo |
131-
| Ctrl/Cmd + S | Export JSON |
132-
| Delete / Backspace | Delete selection |
154+
### Pre-deploy checklist
155+
156+
- [ ] Set `JWT_SECRET` to a strong random value
157+
- [ ] Set `DATABASE_URL` for room persistence
158+
- [ ] Set `REDIS_URL` if running multiple server instances
159+
- [ ] Build with `--release` (WASM + server)
160+
- [ ] Enable HTTPS (required for secure WebSockets in production)
161+
- [ ] Set `REQUIRE_AUTH=1` if you need gated room creation
133162

134-
### Tests
163+
## Tests
135164

136165
```bash
137166
cargo test -p excaildraw-core -p excaildraw-server -p excaildraw-client
138167
cargo clippy -p excaildraw-core -p excaildraw-server -p excaildraw-client --all-targets -- -D warnings
139168
cd crates/client && trunk build --release
140169
```
141170

142-
## Roadmap
143-
144-
See [excaildraw.md](./excaildraw.md) for full architecture. Future work:
145-
146-
- OAuth providers, NATS alternative to Redis
147-
- Rough strokes in SVG export, async image rendering
148-
- Yrs CRDT migration for conflict-free offline edits
149-
150171
## License
151172

152173
MIT

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
3-
"productName": "excaildraw",
3+
"productName": "rustCanvas",
44
"version": "0.1.0",
5-
"identifier": "app.excaildraw.desktop",
5+
"identifier": "app.rustcanvas.desktop",
66
"build": {
77
"frontendDist": "../../../dist",
88
"devUrl": "http://127.0.0.1:3000",
@@ -12,7 +12,7 @@
1212
"app": {
1313
"windows": [
1414
{
15-
"title": "excaildraw",
15+
"title": "rustCanvas",
1616
"width": 1280,
1717
"height": 800,
1818
"resizable": true

crates/client/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version.workspace = true
44
edition.workspace = true
55
license.workspace = true
66
authors.workspace = true
7-
description = "WebAssembly client for excaildraw"
7+
description = "WebAssembly client for rustCanvas"
88

99
[lib]
1010
crate-type = ["cdylib", "rlib"]
@@ -24,7 +24,9 @@ web-sys = { version = "0.3", features = [
2424
"CanvasRenderingContext2d",
2525
"Document",
2626
"Element",
27+
"HtmlElement",
2728
"HtmlAnchorElement",
29+
"Storage",
2830
"HtmlCanvasElement",
2931
"HtmlImageElement",
3032
"HtmlInputElement",

crates/client/index.html

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
6-
<title>excaildraw</title>
6+
<title>rustCanvas</title>
77
<link data-trunk rel="rust" />
88
<link rel="preconnect" href="https://fonts.googleapis.com" />
99
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -29,13 +29,14 @@
2929
--tool-size: 36px;
3030
}
3131

32-
.app.dark {
32+
html.dark {
3333
--color-surface: #1e1e1e;
3434
--color-surface-low: #121212;
3535
--color-border: rgba(255, 255, 255, 0.1);
3636
--color-text: #ececec;
3737
--color-text-muted: #9b9b9b;
38-
--color-primary-light: rgba(105, 101, 219, 0.2);
38+
--color-primary-light: rgba(105, 101, 219, 0.25);
39+
--color-primary-darker: #8b87e8;
3940
--canvas-bg: #121212;
4041
}
4142

@@ -67,7 +68,9 @@
6768
transition: background 0.15s, border-color 0.15s;
6869
}
6970
.chip:hover { background: var(--color-surface-low); border-color: rgba(0,0,0,0.12); }
70-
.app.dark .chip:hover { border-color: rgba(255,255,255,0.18); }
71+
html.dark .chip:hover { border-color: rgba(255,255,255,0.18); }
72+
html.dark .tool-btn:hover { color: #8b87e8; }
73+
html.dark .room-pass::placeholder { color: var(--color-text-muted); }
7174
.chip.primary { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
7275
.chip.primary:hover { background: var(--color-primary-darker); }
7376
.room-pass {

crates/client/src/app.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ pub fn app() -> Html {
217217
})
218218
};
219219

220+
{
221+
let theme = theme.clone();
222+
use_effect_with((), move |_| {
223+
crate::theme::apply_document_class(theme.borrow().dark);
224+
|| ()
225+
});
226+
}
227+
220228
{
221229
let canvas_ref = canvas_ref.clone();
222230
let viewport = viewport.clone();
@@ -780,7 +788,7 @@ pub fn app() -> Html {
780788
}
781789
}
782790
"toggle-theme" => {
783-
theme.borrow_mut().dark = !theme.borrow().dark;
791+
theme.borrow_mut().toggle();
784792
bump_frame();
785793
}
786794
"collab" => {
@@ -1029,9 +1037,9 @@ pub fn app() -> Html {
10291037
let dark_canvas = theme_snapshot.dark;
10301038

10311039
html! {
1032-
<div class={classes!("app", dark_canvas.then_some("dark"))} ref={app_ref}>
1040+
<div class="app" ref={app_ref}>
10331041
<header class="top-bar">
1034-
<div class="brand">{"ex"}<span>{"caildraw"}</span></div>
1042+
<div class="brand">{"rust"}<span>{"Canvas"}</span></div>
10351043
<div class="chip-group">
10361044
<button type="button" class="chip" data-action="undo">{"Undo"}</button>
10371045
<button type="button" class="chip" data-action="redo">{"Redo"}</button>

crates/client/src/crypto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use sha2::{Digest, Sha256};
77

88
fn derive_key(password: &str, room_id: &str) -> [u8; 32] {
99
let mut hasher = Sha256::new();
10-
hasher.update(format!("excaildraw:{room_id}:{password}"));
10+
hasher.update(format!("rustcanvas:{room_id}:{password}"));
1111
hasher.finalize().into()
1212
}
1313

crates/client/src/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use indexed_db_futures::prelude::*;
22
use wasm_bindgen::JsValue;
33

4-
const DB_NAME: &str = "excaildraw";
4+
const DB_NAME: &str = "rustcanvas";
55
const STORE: &str = "kv";
66

77
async fn open_db() -> Result<IdbDatabase, JsValue> {

0 commit comments

Comments
 (0)