███████ ██████ ██ ██ ██ ██████ ██████ ██ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ ██ ██ ██ ██ ██ ██ ███ ██ ███ ██ █████
██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ ██████ ██████ ██ ██████ ██████ ███████ ███████
▀▀
draw. guess. repeat.
A minimal, real-time multiplayer draw & guess game. Create a private room, share the code, take turns drawing while everyone else tries to guess the word.
The word bank lives entirely server-side — players can't cheat by reading the source.
- real-time canvas synced across all players via Firebase Realtime Database
- private rooms with a 6-character shareable code
- host controls — rounds, draw time, hints, word count, max players, categories
- custom word support per room
- score system with guess-order bonuses and drawer rewards
- progressive hints that reveal letters over time
- emoji reactions, vote-to-skip, live chat
- word bank stored in Firestore, fetched server-side only — never in the client bundle
| framework | Next.js 15 (App Router) |
| realtime | Firebase Realtime Database |
| word bank | Firebase Firestore (server-side only) |
| server | Firebase Admin SDK |
| styling | Tailwind CSS v4 |
| language | TypeScript 5 |
git clone https://github.com/thor-op/squiggle.git
cd squiggle
npm install- Create a project at console.firebase.google.com
- Enable Realtime Database
- Enable Firestore in Native mode (for the word bank)
- Go to Project Settings → Service Accounts → Generate new private key
cp .env.example .env.localFill in .env.local with your Firebase client config and Admin SDK credentials. Make sure NEXT_PUBLIC_FIREBASE_DATABASE_URL is set — it's the RTDB URL from your Firebase console.
npx firebase login
npx firebase use your-project-id
# deploy both RTDB and Firestore rules
npx firebase deploy --only database,firestore:rulesWords live in Firestore (server-side only), not in the client bundle. Copy the example and fill in your words:
cp scripts/seed-words.example.ts scripts/seed-words.ts
# edit scripts/seed-words.ts with your word bank
npm run seed
scripts/seed-words.tsis gitignored — your actual words stay private. Only the example is committed.
npm run devapp/
├── api/game/route.ts server-side logic (word validation, scoring, hints)
├── room/[roomId]/page.tsx game room
└── page.tsx landing page
components/
├── Canvas.tsx drawing canvas (RTDB synced)
├── Chat.tsx chat + guess input (RTDB synced)
├── HostControls.tsx lobby settings
├── FloatingEmojis.tsx emoji reactions
└── RoomDoodles.tsx decorative background
lib/
├── firebase.ts RTDB client instance
├── firebaseAdmin.ts admin SDK init (RTDB + Firestore)
├── gameLogic.ts room creation, turns, chat
├── words.ts getWordMask utility only
├── sounds.ts web audio sound effects
├── store.ts session storage helpers
└── types.ts shared typescript types
scripts/
├── seed-words.example.ts template — copy and fill in your words
└── seed-words.ts your actual word bank (gitignored)
| data | where | why |
|---|---|---|
| rooms, players, chat, canvas | Firebase Realtime Database | low-latency, push-based sync |
| word bank | Firestore (admin-only) | server-side only, clients blocked by rules |
| secret word (active round) | server memory + Firestore secrets | never exposed to clients |
Words are seeded into Firestore under wordBank/{category} via the Admin SDK. Firestore rules block all client reads on that collection. The client bundle contains zero words — only a getWordMask utility that turns a word into underscores.
To update words, edit scripts/seed-words.ts and re-run npm run seed.
bugs and feedback welcome via github issues.
if squiggle brings you joy, consider supporting development.
AGPL v3 — if you modify and deploy this, you must open source your changes under the same license.
made with ♥ by thor-op