Implements an autosave mechanism to prevent data loss by periodically sending snapshots of the drawing to the backend. This includes new API endpoints on the server and updated frontend services. Additionally, improves the editor experience with: - Enhanced CSRF protection and origin validation in the backend. - Fix for React "Maximum update depth exceeded" error during scene mutations using a mutation guard. - New presentation slide thumbnails and navigation UI. - Expanded template library with various brainstorming layouts. - Refined dashboard statistics and layout styling. - Improved sidebar logo using SVG for better scaling.
Excalidraw FULL
A self-hosted visual workspace for drawing, planning, mapping, charting, and lightweight project management built around a fast collaborative canvas.
What It Is
Excalidraw FULL is a production-grade visual workspace platform. It is no longer a simple drawing wrapper — it is a full product with backend-owned persistence, team collaboration, permissions, and structured file management.
- Visual canvas first — freeform drawing remains central
- Backend-owned persistence only — no browser/local save modes
- Team collaboration native — real-time editing with presence
- File/folder/project management built in — hierarchical organization via
/folder/:folderId/drawing/:drawingId - Secure sharing and permissioning — explicit grants with inheritance
- Activity history and auditability — every action is tracked
- Templates and structured productivity — system + team + personal templates
- Rich linking between canvases — embeds, references, knowledge graph
- Command palette — global
Cmd/Ctrl+Kfor power users - Fulltext search — find drawings from anywhere
- Revision browser — time-travel through drawing history with one-click restore
- Dark mode — persistent theme preference across sessions
- Presenter notes — add notes to any drawing for presentations
- Responsive layout — mobile sidebar toggle, adaptive grids
- Accessibility — ARIA labels, roles, keyboard navigation
- Self-hosting — single Docker image, healthchecks, volume mounts
Quick Start
git clone https://github.com/BetterAndBetterII/excalidraw-full.git
cd excalidraw-full
cp .env.example .env
# Edit .env and set JWT_SECRET (required)
# openssl rand -base64 32
make build # Build frontend + Go binary
make test # Run all tests
make docker-up # Or run via Docker Compose
The application will be available at http://localhost:3002.
Quick Start with Docker (Pre-built Image)
Run the latest pre-built image without cloning or building:
1. Create the Docker Compose file
services:
postgres:
image: postgres:16-alpine
container_name: excalidraw-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-excalidraw}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-excalidraw}
POSTGRES_DB: ${POSTGRES_DB:-excalidraw}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
excalidraw:
image: ghcr.io/dvorinka/excalidraw-full:latest
container_name: excalidraw-app
restart: unless-stopped
ports:
- "${PORT:-3002}:3002"
environment:
- LISTEN_ADDR=:3002
- STORAGE_TYPE=postgres
- DATABASE_URL=postgres://${POSTGRES_USER:-excalidraw}:${POSTGRES_PASSWORD:-excalidraw}@postgres:5432/${POSTGRES_DB:-excalidraw}?sslmode=disable
- JWT_SECRET=${JWT_SECRET}
# Optional: GitHub OAuth
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
# Optional: Generic OIDC
- OIDC_ISSUER_URL=${OIDC_ISSUER_URL:-}
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-}
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-}
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
2. Set Environment Variables
For Dokploy/CasaOS: Configure these in the UI under Environment Variables.
For CLI/Terminal: Create a .env file:
cat > .env << EOF
# Required: Generate with: openssl rand -base64 32
JWT_SECRET=your-secure-random-string-min-32-chars
# Optional: Change defaults or leave as-is
POSTGRES_USER=excalidraw
POSTGRES_PASSWORD=excalidraw
POSTGRES_DB=excalidraw
PORT=3002
# Optional: GitHub OAuth (for social login)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Optional: Generic OIDC (for SSO)
OIDC_ISSUER_URL=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
EOF
3. Start the services
docker compose up -d
The app will be available at http://localhost:3002 (or your configured PORT).
Docker Images
Pushing to GitHub automatically builds and publishes the production image to GitHub Container Registry:
ghcr.io/<owner>/<repo>:latest
ghcr.io/<owner>/<repo>:sha-<commit>
ghcr.io/<owner>/<repo>:<branch>
No registry secrets are required. The workflow uses GitHub's built-in GITHUB_TOKEN.
Requirements
- Go 1.26.2+
- Node.js 20+ (for frontend build)
- Make (optional, for convenience commands)
- Docker (optional, for containerized deployment)
Configuration
All configuration is via environment variables. See .env.example for the full reference.
| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | Secure random string for session signing (min 32 chars) |
STORAGE_TYPE |
No | postgres (default), memory, filesystem, s3 |
DATABASE_URL |
Yes for Postgres | PostgreSQL connection string |
GITHUB_CLIENT_ID |
No* | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET |
No* | GitHub OAuth app client secret |
OIDC_ISSUER_URL |
No* | Generic OIDC issuer for SSO |
OIDC_CLIENT_ID |
No* | OIDC client ID |
OIDC_CLIENT_SECRET |
No* | OIDC client secret |
ALLOWED_ORIGINS |
No | Comma-separated CORS origins |
LISTEN_ADDR |
No | Server bind address (default :3002) |
* At least one external auth provider (GitHub OAuth or OIDC) OR use built-in password authentication. Password auth works out of the box.
Architecture
- Backend: Go 1.26.2, Chi router, PostgreSQL (pgx)
- Frontend: React 18, Vite, TypeScript, Zustand, react-router-dom, react-i18next
- Real-time: Socket.IO for collaborative canvas sync
- Auth: Session cookies (httpOnly, SameSite=Lax) + bcrypt password hashing + OAuth/OIDC
- Storage: PostgreSQL default, with legacy filesystem/S3 options for canvas storage
- API spec: OpenAPI 3.0 in
api/openapi.yaml; TypeScript client viamake generate-api-client
API
The workspace API is mounted at /api and requires session authentication. Key endpoints:
POST /api/auth/signup/POST /api/auth/login/POST /api/auth/logoutGET /api/auth/me— current userGET /api/teams,POST /api/teams,GET /api/teams/:id/membersGET /api/drawings,POST /api/drawings,GET /api/drawings/:idPATCH /api/drawings/:id,DELETE /api/drawings/:idGET /api/drawings/:id/revisions,POST /api/drawings/:id/revisionsGET /api/drawings/:id/permissions,POST /api/drawings/:id/permissionsGET /api/drawings/:id/share-links,POST /api/drawings/:id/share-linksGET /api/search?q=— fulltext searchGET /api/folders,POST /api/foldersGET /api/projects,POST /api/projectsGET /api/templates— system, team, and personal templatesGET /api/activity— audit trail with actor hydrationGET /api/stats— workspace statistics (counts + storage)GET /api/health— readiness probe
Development
# Terminal 1: Go backend with auto-reload (requires air)
make dev-backend
# Terminal 2: Vite dev server
make dev-frontend
# Or run both manually:
go run main.go # backend on :3002
cd frontend && npm run dev # frontend on :5173
Building
make build # Full production build (frontend + Go binary)
make build-frontend # React build only
make build-backend # Go binary only
make build-docker # Docker image locally
make test # Run Go + frontend tests
make test-backend # Go unit tests
make test-frontend # Vitest unit tests
make test-e2e # Playwright E2E tests
make lint # Run all linters
make fmt # Format all code
make generate-api-client # TS client from openapi.yaml
make clean # Remove build artifacts
make docker-up # Docker Compose local run
make docker-down # Stop Docker Compose
make docker-logs # Tail Docker logs
make help # Show all targets
Project Structure
.
├── main.go # Go server entrypoint
├── workspace/ # Core domain: models, store, HTTP handlers, tests
│ ├── models.go
│ ├── store.go # PostgreSQL persistence + migrations
│ ├── store_sharing.go # Permissions, share links, embeds
│ ├── http.go # API route handlers
│ ├── http_extra.go # Search, stats, activity
│ ├── stats.go # Workspace statistics
│ ├── rate_limiter.go # Auth endpoint rate limiting
│ └── *_test.go # Go unit tests
├── middleware/ # Auth, security headers
├── handlers/ # Legacy firebase, kv, auth
├── frontend/ # React + Vite frontend
│ ├── src/
│ │ ├── pages/ # Dashboard, Editor, Auth, Settings, etc.
│ │ ├── components/ # Reusable UI (Button, Card, CommandPalette, etc.)
│ │ ├── stores/ # Zustand state management
│ │ ├── services/ # API client
│ │ ├── i18n/ # Translation files (en.json)
│ │ └── styles/ # Global SCSS + CSS variables
│ └── package.json
├── api/
│ └── openapi.yaml # Full OpenAPI 3.0 spec
├── excalidraw-full.Dockerfile # Multi-stage production build
├── docker-compose.yml # Local Docker Compose
├── Makefile # Build, test, dev automation
└── .env.example # Environment reference
Security
- bcrypt(cost=12) password hashing
- httpOnly, SameSite=Lax session cookies
- CSRF-like same-origin mutation checks on state-changing requests
- URL sanitization for embeds (blocks file://, javascript:, private IPs)
- Content-Security-Policy headers with strict defaults
- Rate limiting on auth endpoints (10 req / 15 min per IP)
- Permission matrix with explicit grants + inheritance
- All mutations require authenticated session
Internationalization
Frontend uses react-i18next with i18next-browser-languagedetector. All UI strings are externalized to frontend/src/i18n/locales/en.json. Add new keys there and reference via t('key').
License
MIT