Realtime / Computer Vision
SulyapPB
Photobooths need two people in one place. This one doesn't.
DisclaimerLive peer-to-peer video is not enabled in the public demo: it needs a paid TURN relay. I'm planning to use Cloudflare, but I haven't set it up yet.




A photobooth for two people who are nowhere near each other. One person makes a room, sends the 6-character code to their partner, and both phones count down together. You snap on yours, they snap on theirs, and one classic 2×6 photo strip comes out with both of you on it.
- 01Host a room. A short code is minted with no O/0 and no I/1/L, so it survives being read off one phone screen and typed into another. Rooms live 60 minutes.
- 02Both join. The guest enters the code, and a presence heartbeat every 7s tells each side the other is really there before anything can start.
- 03Pick the look together. A frame for the paper and a backdrop for the picture, chosen once for the room and mirrored to both devices, so a cut never comes out half velvet and half kitchen.
- 04Count down as one. The host arms a countdown with ~200ms of slack baked in so both phones land on the same instant despite the network hop.
- 05Snap ×4. Each device captures its own half locally, and only the finished cut crosses the wire. Two halves are stitched into each of the 4 frames, filling a live strip as you go.
- 06Download. The strip is composed on a canvas at 600×1800 (2×6 in at 300dpi) and saved as a PNG. The on-screen strip is fluid CSS while the download path is exact pixels, deliberately two separate renderers.
- 07Nothing is stored. Photos pass browser to browser over Realtime broadcast and are never written to a database. There is no schema for them to go into.
Core
- Next.js 15: App Router, Turbopack
- React 19
- TypeScript, strict
- ESLint 9 with eslint-config-next
Styling
- Tailwind CSS v4 via @tailwindcss/postcss
- clsx + tailwind-merge for class composition
- Hand-rolled keyframes in globals.css: rise, tick, float
Graphics & motion
- @mediapipe/tasks-vision: selfie segmentation, cutting the person out at 256×256, running as a module-level singleton so one model and GPU context serves the preview, the capture, and the peer stream
- Canvas 2D compositor: mask, paint backdrop, person on top, producing a canvas that stands in for the camera everywhere the camera used to be read
- Backdrops are painted gradients rather than photos - free to download, sharp at any size, and forgiving of the hair-edge artifacts no segmenter gets exactly right
- Full graceful degradation: if the model won't load, the WASM won't compile, or the phone's too slow, you land back on the raw camera, which is still a working photobooth
Backend & data
- Supabase Realtime (broadcast only) as the room transport, swapping in automatically the moment the two NEXT_PUBLIC_SUPABASE_* vars exist
- BroadcastChannel fallback transport for single-device and cross-tab dev, so the whole booth was testable before any backend existed. The booth talks to one RoomTransport interface and never learns which it got.
- Supabase Postgres with a service-role client for the licenses table only: RLS-free, server-only, with a runtime throw if it is ever imported in a browser
- PayMongo for premium (GCash, card, PayMaya, GrabPay) at ₱149. Codes are minted in the signature-verified webhook, never at checkout.
- Resend for the feedback form, with a mailto fallback when unkeyed
- In-memory per-IP throttling on the checkout and feedback routes
Deploy
- Vercel