DrawSketch
Browser-based vector drawing app with cloud sync
A canvas-based drawing app with a file-system organizer, cloud sync, and public sharing — works offline without an account.
About the Project
DrawSketch is a browser-based drawing application built on HTML Canvas and Rough.js. It supports 13 drawing tools, a nested file/folder system, undo/redo history, and exports to PNG, SVG, JSON, and ZIP.
Anyone who wants a local-first sketching tool that optionally syncs to the cloud. The app is fully usable without an account, storing all data in localStorage.
The project was built to explore implementing a canvas drawing engine from scratch — no third-party whiteboard library — paired with a production-grade backend that handles authentication, file persistence, and public sharing.
What I Built
Built the entire rendering layer using raw HTML Canvas and Rough.js, including element hit-testing, viewport transforms, alignment guides, and a rAF-based draw loop that avoids React re-renders during drag.
Designed the REST API using Express 5 and Prisma with MongoDB. Implemented a full auth system (JWT, refresh tokens, OTP email verification, Redis token blacklist) and file/folder CRUD with ownership checks.
Implemented a debounced canvas sync hook that writes to localStorage immediately and pushes to the API after 2 seconds of inactivity. The file-system hook keeps a local-to-remote ID map for offline/online reconciliation.
Containerized the backend with a multi-stage Docker build (Chainguard builder, Distroless production runtime). Configured Docker Compose with health-checked MongoDB and Redis services.
What It Can Do
Drawing tools: pencil (freehand), rectangle, ellipse, diamond, triangle, line, arrow, text, image, frame, eraser, select, hand
Per-file undo/redo history stack (up to 80 entries) with history seeding on file open
File tree with nested folders, drag-and-drop moves, rename, duplicate, and search
Export to PNG, SVG, JSON (.excalidraw), and ZIP archive of all files
Engineering Deep Dive
Click any card to read the full technical detail.
Challenges & Trade-offs
The real decisions behind the architecture.
How to sync canvas changes to the API without blocking the drawing experience
Used a 2-second debounced effect (useSync hook) that fires only when the authenticated user modifies an open file with a known remote ID, writing to localStorage synchronously and deferring the API call
A crash within 2 seconds of the last edit could lose the most recent stroke, but localStorage always has the current state so data loss is bounded to the API copy only
Keeping drag animations smooth without bypassing React's state model entirely
Maintained parallel refs (elementsRef, vpRef, liveOffsetRef) that mirror React state and are read directly by the rAF draw loop, committing back to React state only on mouseUp
Two sources of truth (refs and state) require careful synchronization via effects, but the result is jank-free panning and moving of elements
File IDs differ between localStorage (nanoid) and MongoDB (ObjectId)
When authenticated the useFileSystem hook uses the backend ID directly as the local ID and stores a ds_remote_ids map. When offline, nanoid IDs are used and the map is empty
Simplifies online lookups but requires clearing and re-bootstrapping the ID map on login/logout
Text editing on canvas requires a real textarea for IME and accessibility support, but committing text must not double-fire when blur is triggered by the same mouseDown that calls commitText
Introduced a committingRef boolean flag that is set before calling commitText on mouseDown. The textarea's onBlur handler skips the second commit if the flag is already true
Text is committed exactly once whether the user clicks away or presses Enter/Escape
Arrow elements bound to shapes must follow their target shapes when those shapes are moved
During live drag rendering, the draw loop inspects boundStartId and boundEndId on arrow elements and applies the same dx/dy offset to the bound endpoints. On mouseUp, the final positions are written to React state
Arrows stay connected to their bound shapes throughout a move operation without modifying the element array during drag
Importing Excalidraw-format files into a different schema
The importJSON function parses the type: excalidraw discriminator and maps Excalidraw fields to the internal schema, including converting angle (radians) to rotation (degrees), freedraw to pencil, and fontFamily integers to CSS strings
Users can import standard .excalidraw files and have them open correctly in DrawSketch
System Design
What Makes It Fast
Selection rectangle is drawn on a separate transparent overlay canvas via rAF, so rubber-band selection never triggers a full main-canvas redraw
Response-time middleware categorizes each request as VERY FAST / FAST / NORMAL / SLOW / VERY_SLOW / CRITICAL and emits a SLOW_RESPONSE warning log for requests over 1 second
Winston uses daily-rotating file transports: info logs rotate daily and are kept 14 days; error logs are gzip-archived and kept 30 days
The Docker production image uses Distroless (nonroot) as the runtime, reducing attack surface and image size compared to a full Node image
Security Model
Helmet sets secure HTTP headers; HPP prevents HTTP parameter pollution
Two separate rate limiters: a general limiter (100 req/15 min) on all /api routes, and a stricter auth limiter (10 req/15 min) on login, register, forgot-password, resend-otp, and verify-email
Logout blacklists the access token in Redis with a TTL equal to the token's remaining lifetime; the auth middleware checks the blacklist on every protected request
Forgot-password always returns the same generic message regardless of whether the email exists, preventing user enumeration
How It Ships
Screenshots




