Project Overview
Happy Doodle is a 30-second doodle battle that turns a crowd into judges.
Two players scan a QR, draw on their phones, and at the buzzer confetti pops, both drawings are revealed, and everyone else votes live. When the dust settles, the app stitches a shareable “battle poster.”
I built this to break the ice at the DevNetwork [API + Cloud + Data] Hackathon.
As a student/early-career dev in San Jose, I wanted something fun, memorable, and technical that could help me meet people (and maybe win a sponsor prize). A quick, social game people can join with a scan felt perfect.
Inspiration
Events are full of incredible people who don’t know each other yet. A silly, low-pressure game can start conversations instantly.
I love lightweight real-time apps — “votes flying in live” feels magical and makes the project come alive.
I also wanted something I could demo in 90 seconds and deploy to the cloud with real URLs and QR codes.
What It Does (One Round)
- Host clicks Create Room → app shows QR codes for Players and Audience
- Two players join (Left/Right), pick color/brush, and get a random prompt
- Host taps Start 30s Round → timer ticks, players draw on their phones
- At 0s → both canvases reveal with confetti; audience scans Vote QR
- Votes stream in live; winner is announced
- Share Battle Image creates a stitched PNG for posting
How I Built It
Stack
- Next.js 15 (App Router), TypeScript, TailwindCSS
- Supabase: Postgres, Realtime (votes), Storage (battle images), RLS policies
- Deployed on Vercel; links/QRs automatically use the current host
Key pieces
src/app/page.tsx— room creation + QR codessrc/app/room/[id]/page.tsx— dual canvases, timer, prompts, confetti, image stitchingsrc/app/vote/[id]/page.tsx— audience voting + live barsrc/app/api/room/route.ts— inserts a room and returns{ joinUrl, spectateUrl }derived from the request origin
Data model (simplified)
create table if not exists public.rooms (
id uuid primary key default gen_random_uuid(),
created_at timestamptz not null default now(),
status text not null check (status in ('open','drawing','reveal','closed')),
prompt_text text
);
create table if not exists public.votes (
room_id uuid references public.rooms(id) on delete cascade,
voter_hash text not null,
vote_for text check (vote_for in ('left','right')),
created_at timestamptz not null default now(),
primary key (room_id, voter_hash)
);
-- Hackathon-friendly RLS
alter table public.rooms enable row level security;
alter table public.votes enable row level security;
create policy "rooms_read" on public.rooms for select using (true);
create policy "rooms_write" on public.rooms for insert with check (true);
create policy "votes_read" on public.votes for select using (true);
create policy "votes_upsert"on public.votes for insert with check (true);
Realtime & Poster
Realtime
The vote page subscribes to postgres_changes on votes filtered by room_id.
Each insert bumps the left/right counters instantly.
Battle Poster
After reveal, the app:
- Draws both canvases onto an off-screen canvas
- Adds labels
- Uploads a PNG to Supabase Storage (
battlesbucket) - Returns the public URL for sharing
Challenges & How I Solved Them
- Localhost links on phones → early links pointed to
http://localhost:3000.
Fixed by computing links from the request origin:
``const { protocol, host } = new URL(req.url); const base =${protocol}//${host}`; Mobile randomUUID & SSR pitfalls → accessing localStorage/crypto during SSR caused errors. Moved device-ID logic into client-only hooks with useEffect.
Supabase policies & Realtime → RLS blocked everything at first. Added permissive hackathon policies + enabled Realtime on votes.
Image upload permissions → stitched PNG uploads failed. Created public bucket + storage insert/update policies for battles.
Type issues in prod build → canvas-confetti lacked types. Added a tiny .d.ts shim.
GitHub/Vercel deploy issues → token/remote errors. Switched to gh (GitHub CLI) for clean auth + deploy from main.
What I Learned
- Design for demoability → a project that on-boards with a QR code and ends with confetti + a shareable artifact is perfect for hackathons.
- Small details = big UX → using request origin for links/QRs and responsive canvases made it work on everyone’s phones.
- Supabase is a fast real-time backend → Postgres + Realtime + Storage + RLS = full stack in hours.
- Production mindset → avoid hard-coded hosts, isolate client-only code, and apply minimal but correct security rules—even for a hackathon.
What’s Next
- Sponsor hook: Vonage – send the battle poster via SMS/MMS
- Sponsor hook: Foxit – playful “Battle Certificate” PDF
- Prompt packs (e.g., Sci-Fi, Food, San Jose)
- Light anti-spam (rate limit, dedupe)
- Spectator gallery / replay with most-voted battles
❤ Why It Matters ❤
Happy Doodle makes people smile in under a minute and gets strangers talking.
It’s a tiny app that showcases real-time UX, cloud APIs, and it is a great way to meet new people

Log in or sign up for Devpost to join the conversation.