Flappy Bird for push day — control the bird with push-ups via your webcam (or phone camera) and MediaPipe Pose.
Inspired by the viral “Push day killer” camera filter: bird Y follows your torso height; copper pipes scroll; score for clearing gaps. Geometric bird/pipes (not copyrighted Flappy Bird sprites).
Live: https://pushflappy.com
Repo: https://github.com/davidemanuelDEV/push-flappy
npm install
npm run devOpen http://localhost:3000 → Start.
Production build:
npm run build && npm start- On wipeout, the run is POSTed once to
/api/leaderboardautomatically (saved nick/emoji, default Anon / 🐦). The wipeout Post CTA shows Posted / disabled after success — Challenge a friend stays the filled primary. - On wipeout, Share challenge builds a deep link:
https://pushflappy.com/play?beat={score}(optional&reps=). In a race, the primary CTA shares the race link instead (/race/{id}). - Share text includes score + wipeout quip + dare CTA. Prefer
navigator.share; clipboard fallback. - Opening
/play?beat=Nshows a bar-to-beat in the HUD. Clearing a higher score triggers a you beat them share prompt. - Wipeout / victory share UI includes a secondary Squat Flappy outline row under Challenge a friend (Growth lock: “Arms cooked? Legs next → squatflappy.com”) →
https://squatflappy.com/play. - Landing hero (and
/playready/idle chrome) shows a rounded-full Also play Squat Flappy promo pill →https://squatflappy.com/play. Hidden during active play. - Optional canvas share card (1080×1080) attaches when the OS share sheet supports files.
- Pipe gap RNG is seeded from the calendar day in America/Los_Angeles (
YYYY-MM-DD). - Everyone faces the same pipe layout that day → scores are comparable on the daily board.
/racemints a short id and copieshttps://pushflappy.com/race/{id}./race/[id]is the dare-first page: a real nick (2–16 letters/numbers) is required. Join POSTs that nick at score 0 / reps 0 so the live top-10 has names before anyone flies. Play (/play?race={id}) stays off until join succeeds. Spectators need no camera.- Same pipe seed for everyone in that race.
/play?race=shows a compact live top-10 during ready and play (your row highlighted). Scores POST as you clear pipes (~1s) and on wipeout — latest per nick, cap 10, same nick updates in place. Spectators on/race/{id}and/overlaypoll the same board (~1s). Not frame-sync. - OBS:
/race/[id]/overlayor/race/[id]?obs=1— big ranks, no camera. - Persistence: same Blob / KV / memory backends as the daily board (
push-flappy/race/{id}.json).
GET /api/leaderboard?day=YYYY-MM-DD— top scores for that day.POST /api/leaderboard—{ nick, emoji?, score, reps, dayKey? }(today only). Anonymous nick (2–16 chars); no accounts.- In-memory IP rate limits (GET ~60/min, POST ~12/min per IP).
- Best score per nick kept per day.
- Compact opt-in on game over, plus light forms on
/boardand the landing page. - Copy: “Get reminder to challenge again” → success: “You’re on the list”.
- No accounts. Capture only unless Resend is configured.
POST /api/reminders— body:{ "email": "you@example.com", "source": "gameover|board|landing", "timezone?": "America/Los_Angeles", "dayKey?": "YYYY-MM-DD" }- Validates email; IP rate-limit (~8/min); dedupes by normalized email.
- Stores
email,createdAt,dayKey,timezone(default America/Los_Angeles),source. - Persistence: same KV env as the leaderboard (
KV_REST_API_URL/KV_REST_API_TOKEN). Without KV → in-memory (not durable across cold starts).
| Env var | Purpose |
|---|---|
KV_REST_API_URL |
Upstash / Vercel KV REST URL (required for durable reminder storage) |
KV_REST_API_TOKEN |
REST token |
UPSTASH_REDIS_REST_URL |
Alias for KV_REST_API_URL (raw Upstash Redis REST) |
UPSTASH_REDIS_REST_TOKEN |
Alias for KV_REST_API_TOKEN |
RESEND_API_KEY |
Optional. If set, sends a one-line welcome. If absent, capture-only — wire a cron later to email “Push day — beat today’s board”. |
RESEND_FROM |
Optional From header for Resend (defaults to Resend onboarding address) |
Production paths avoid logging full emails (masked if logged in dev).
Daily scores persist when any of these are set on the Vercel project:
| Env var | Purpose |
|---|---|
BLOB_READ_WRITE_TOKEN |
Private Vercel Blob store (pathname push-flappy/lb/{day}.json) |
KV_REST_API_URL |
Upstash / Vercel KV REST URL |
KV_REST_API_TOKEN |
REST token |
UPSTASH_REDIS_REST_URL |
Alias for KV_REST_API_URL (same REST protocol) |
UPSTASH_REDIS_REST_TOKEN |
Alias for KV_REST_API_TOKEN |
Blob wins when BLOB_READ_WRITE_TOKEN is set; else a complete KV_* or UPSTASH_REDIS_REST_* pair. Memory only if none of those are set. Responses include storage: "blob" | "kv" | "memory" — wipeout treats any non-memory value as posted.
In the Vercel dashboard: connect a private Blob store (or Storage → KV / Upstash), then redeploy. Never commit .env.local.
Demo seeds: fake board rows are off in production unless ALLOW_DEMO_LEADERBOARD=1. In development they still fill an empty board. An empty production board shows clear empty-state copy (not a fake-full list).
| Env var | Purpose |
|---|---|
ALLOW_DEMO_LEADERBOARD |
Set to 1 to force demo seeds even in production (local/staging only) |
David / deploy: Blob token on the project persists the daily board. Reminders still use the KV / Upstash REST pair.
@vercel/analytics+@vercel/speed-insightsin the root layout (free tier).- Client events:
play_start,play_wipeout,challenge_open(beat),share_click(channel wa|x|copy|native|card),board_submit,reminder_optin,sibling_click(from: push,surface: wipeout|victory|landing|play),race_create,race_join,race_score,race_share. - Optional
GET /api/stats—{ dayKey, boardEntriesToday, reminderCount, storage, demoAllowed }(no PII).
Phone browsers require HTTPS (or localhost) for getUserMedia. Deploy to Vercel and open pushflappy.com (or your *.vercel.app URL).
- Laptop first: allow the front camera and get into push-up position facing the webcam.
- Phone: portrait works with the phone on the floor facing up; landscape is optional.
- Face the camera in a push-up plank; hold the top of a push-up until you see Start position set, then hit Start.
Primary control: continuous body-Y tracking (not classic gravity + flap).
- MediaPipe Pose Landmarker on each video frame (WASM + lite model from CDN — no API keys).
- Torso height = midpoint of shoulders (11 & 12), with hip/nose fallbacks.
- Start calibration: hold plank ~1s to lock
upY. - Light EMA smoothing; bird X fixed; pipes scroll with gap collision + scoring.
- Optional rep counter from the same torso signal.
- Short/narrow screens get a slightly larger pipe gap.
- Next.js 15 (App Router) · React 19 · TypeScript · Tailwind CSS v4
@mediapipe/tasks-vision(client-only)- Optional Vercel Blob (daily board) + KV / Upstash (board fallback + reminder emails)
- Optional Resend (
RESEND_API_KEY) for welcome mail; otherwise capture-only - No accounts, no company branding
OBS pack (Phase 1). Live setup: https://pushflappy.com/streamers. Repo notes: STREAMERS.md.
- Play Browser Source: https://pushflappy.com/stream (same as
/play?obs=1) - Recommended size: 1920×1080
- Chest/cam: phone face-up on the floor, or webcam on a plank — allow camera, hold the top of a push-up; existing 3-2-1 starts on its own
- Viewer dare: chat opens the beat-me link on their phone (
/play?beat={score}) - Optional second source: https://pushflappy.com/overlay — read-only daily board, no camera
- Race overlay: https://pushflappy.com/race/{id}/overlay — live race top-10, no camera
Do not add paid Twitch Extensions or a !beat bot here.
| Path | Description |
|---|---|
/ |
Marketing / how-to-play + Start CTA |
/play |
Fullscreen camera game (?beat=N challenge; ?obs=1 capture chrome) |
/stream |
OBS play view (same game, no marketing chrome, larger HUD) — noindex |
/streamers |
Human/SEO OBS setup (Browser Source, cam, plank, overlays) |
/overlay |
Read-only daily board widget for a second Browser Source |
/race |
Mint a race link |
/race/[id] |
Join + live top-10 (camera-free spectate). ?obs=1 = overlay |
/race/[id]/overlay |
OBS race board, no chrome |
/api/leaderboard |
Daily board GET/POST |
/api/race |
POST mint / ensure a race |
/api/race/[id] |
GET board; POST score (0/0 = join, names before play) |
/api/reminders |
Email reminder capture POST |
/board |
Camera-free daily board + reminder opt-in |
/guides/webcam-push-up-game |
How-to: torso→bird, on-device pose |
/guides/obs-push-up-overlay |
OBS Browser Source + overlays |
/guides/async-fitness-race |
Nick-first async race explainer |
/guides/squat-flappy |
Sibling game (squatflappy.com) |