A full-stack collaborative canvas notes app inspired by Excalidraw. Users can sign up, create shareable note rooms, collaborate in real time over WebSockets, persist canvas notes as room history, generate starter diagrams, and ask Gemini questions about saved note text.
- Authenticated signup/signin with JWT and bcrypt password hashing
- Shareable note creation using title-based slugs
- Real-time multi-user drawing over WebSockets
- Canvas tools for pencil, rectangle, circle, line, and text
- Persisted room history backed by PostgreSQL and Prisma
- AI diagram generation from prompts using Gemini
- AI note Q&A that reads saved text shapes from the current canvas
- Turborepo + pnpm workspaces
- Next.js 15, React 19, Tailwind CSS
- Express HTTP API
wsWebSocket server- Prisma + PostgreSQL
- Zod for request and AI output validation
- Gemini 2.5 Flash for AI drawing and note Q&A
The AI features are intentionally server-side:
- The canvas UI sends a prompt and canvas dimensions to
POST /ai/shapes. - The Express backend calls Gemini with a JSON response schema.
- The backend validates the returned JSON with
AiShapesResponseSchema. - The frontend sends each validated shape through the existing WebSocket shape flow.
- Every connected user receives and persists the generated diagram like normal drawing events.
For note Q&A, the canvas UI sends a question to POST /room/:roomId/ai/chat. The backend loads saved Chat.message rows for the room, extracts text shapes, sends that note context to Gemini, and returns an answer grounded in the current canvas note.
This keeps API keys out of the browser and treats model output as untrusted data until it passes validation.
The app talks to Postgres only through the DATABASE_URL in packages/db/.env.
Locally we run Postgres in Docker so you never have to test against the hosted
(Neon) database; in production you swap DATABASE_URL back to your Neon string.
# 1. Start a local Postgres 16 container (see docker-compose.yml)
docker compose up -d
# 2. Point Prisma at the local container
cp packages/db/.env.example packages/db/.env
# 3. Create the tables + the (roomId, id) index in the fresh local DB
cd packages/db && pnpm prisma migrate devA freshly started container has no tables — step 3 builds the schema. If you
see P1001: Can't reach database server at localhost:5432, the container is not
running yet: run docker compose up -d and check docker compose ps shows it as
healthy before migrating.
docker compose up -d # start the DB (data persists in the named volume)
pnpm dev # run the apps against local Postgresdocker compose down stops it (data kept); docker compose down -v also wipes
the data volume for a clean slate.
The single source of truth is DATABASE_URL in packages/db/.env:
| Target | DATABASE_URL |
|---|---|
| Local Docker | postgresql://draftboard:draftboard@localhost:5432/draftboard?schema=public |
| Neon (prod) | postgresql://<user>:<pass>@<host>.neon.tech/<db>?sslmode=require |
To apply migrations you already tested locally to Neon, point DATABASE_URL at
Neon and run pnpm prisma migrate deploy.
Note: the backends also load a root
.envfirst, anddotenvdoes not overwrite an already-set variable — so keepDATABASE_URLonly inpackages/db/.envto avoid a stale root value winning silently.
corepack pnpm installSet your Gemini credentials (and DATABASE_URL if not using the Docker flow above):
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/DB_NAME"
GEMINI_API_KEY="your-api-key"
GEMINI_MODEL="gemini-2.5-flash"Generate Prisma Client:
corepack pnpm --filter @repo/db exec prisma generate --schema=prisma/schema.prismaRun the apps in separate terminals:
corepack pnpm --filter http-backend dev
corepack pnpm --filter ws-backend dev
corepack pnpm --filter excalidrawfe devThe drawing app expects:
- HTTP backend:
http://localhost:4000 - WebSocket backend:
ws://localhost:8080 - Shareable note links:
/note/:slug
corepack pnpm --filter http-backend build
corepack pnpm --filter ws-backend build
corepack pnpm --filter excalidrawfe build