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)

  1. Host clicks Create Room → app shows QR codes for Players and Audience
  2. Two players join (Left/Right), pick color/brush, and get a random prompt
  3. Host taps Start 30s Round → timer ticks, players draw on their phones
  4. At 0s → both canvases reveal with confetti; audience scans Vote QR
  5. Votes stream in live; winner is announced
  6. 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 codes
  • src/app/room/[id]/page.tsx — dual canvases, timer, prompts, confetti, image stitching
  • src/app/vote/[id]/page.tsx — audience voting + live bar
  • src/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:

  1. Draws both canvases onto an off-screen canvas
  2. Adds labels
  3. Uploads a PNG to Supabase Storage (battles bucket)
  4. 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

Built With

Share this project:

Updates