Building Devdle: Notes from the Neon Arcade
Devdle is a Wordle-inspired daily puzzle app I built for developers to solve coding challenges.
It was a super fun project to build, and brand obnoxiously over-the-top, so I figured it would also be fun to do a deep-dive on how I built it, how it operates and where it's hosted. Enjoy.
TL;DR
- Edge-first stack (Cloudflare Worker + Pages) keeps the daily puzzle snappy worldwide.
- Puzzles are generated on demand with OpenAI, validated locally, and cached in KV/D1.
- React + Vite powers a CRT-styled UI; all answer checking happens client-side with a public salt.
- Google Analytics events (
app_open,play_click,first_attempt,guess, etc.) make DAU and funnel tracking explicit. - Durable Object rate limiting + cron jobs keep the API steady without babysitting servers.
What I Wanted
- One fresh coding puzzle per UTC day.
- Zero login, zero friction, works on a phone.
- A restrained scope I can support while everything else is on fire.
So Devdle is intentionally small: neon pixels, six guesses, a shareable emoji card. No accounts, no leaderboard.
Architecture Snapshot
| Layer | Stack | Why |
|---|---|---|
| Frontend | React 18, Vite, TypeScript, Tailwind | Fast DX, easy animation, modern tooling. |
| API | Cloudflare Worker (Hono), KV, D1, Durable Object | Edge latency, no cold starts, free-ish to run. |
| Content | OpenAI chat completions | Flexible copy + code snippets with explanations. |
| Hosting | Cloudflare Pages | Static deployment, automatic SSL, custom domain ready. |
| Telemetry | Google Analytics (gtag) + Worker logs | Simple, keeps us honest. |
Everything sits under devdle.iamdevloper.io. Worker handles /api/*; Pages serves the SPA.
Daily Puzzle Lifecycle
- Player hits the SPA →
initAnalytics()→trackEvent('app_open'). - App calls
GET /api/today. - Worker tries:
- KV cache (12h TTL),
- otherwise D1 lookup,
- otherwise generate new puzzle.
- Generation pipeline:
- Pull recent puzzle metadata for variety.
- Call OpenAI with hard constraints: language ∈ {js, py, go}, type
mcq, six choices, one answer. - Validate with Zod → enforce output rules (single-line hashing).
- Run novelty guard (fingerprints, stems). The guard now logs and adjusts difficulty rather than throwing a tantrum.
- Persist to D1, hydrate KV, return puzzle minus the answer but include a short hash.
- Client hashes guesses (
SHA-256(answer|salt), truncated 16 chars) to compare locally. Salt is public but rotated annually. - Wins/losses, streaks, history saved in
localStorage; share text built with emoji tiles.
Backend: Cloudflare Worker
- KV – Hot cache for today's puzzle (keys:
puzzle:YYYY-MM-DD). - D1 – SQLite schema:
puzzles(date TEXT PRIMARY KEY, json TEXT, created_at INTEGER). - Durable Object – Token bucket per IP, 60 req/min (120 for the
/todayread path). - Cron – Runs at 00/08/16 UTC. First call warms KV for current day (idempotent). Second call pre-generates +3 days to keep content ready.
- Secrets –
VITE_API_ORIGIN,OPENAI_API_KEY,DAY_PUBLIC_SALT,SECRET_SALTmanaged via Wrangler.
Worker code paths are heavily logged (console.log, console.warn, console.error). observability.enabled = true makes Cloudflare's dashboards useful when generation misbehaves.
Frontend Bits
App.tsx– Controls view switching (splash,play,stats). Persists attempts/streak history.first_attemptGA event fires the moment someone submits a guess.PuzzleScreen– Input modes:options→ multiple choice,order→ newly added drag/select UI with proper reordering controls,free→ single-line (multiline for sequences),lines→ spot-the-bug.
- Styling – Tailwind with a custom palette (
ink,neonBlue, etc.).BackgroundScene.tsxuses Three.js to render the grid + holograms. - Easter eggs – Console injection (
window.devdle.coin(),devdle.konami()) plus the usual CRT scanlines. - Share – Emoji tiles + UTMed link built via
buildShareText.
Analytics (GA4)
| Event | Trigger |
|---|---|
app_open |
SPA load. |
play_click |
Player hits the Play button. |
first_attempt |
First guess for the day (captures date/type/language). |
guess |
Every guess: result (correct/wrong), attempt number, puzzle metadata. |
share_click |
From play or stats view. |
stats_click, stats_back |
Navigation between panels. |
page_view |
Splash/Play/Stats transitions. |
These keep DAU and funnel numbers straightforward when looking at GA.
Content Guardrails
- Language rotation – always JS, Python, or Go. Hints steer the model away from overusing previous languages.
conceptFingerprint– stored on puzzle metadata; helps avoid repeatingarray-filter-reduce.- Single-answer enforcement –
enforceOutputPuzzlenow converts accidentaloutputpuzzles into MCQs on the fly. ensureFingerprint/enforceNovelty– logs when it repeats itself, downgrades trivial puzzles to difficulty 1.- Choice normalization – Options padded to six, duplicates trimmed, correct answer guaranteed present.
Operations & Testing
- TypeScript across both app and worker (
npm run typecheck). - Worker unit tests (
node --test) cover output enforcement and novelty utilities. - Manual QA for puzzle generation whenever prompt or enforcement logic changes.
- OG/Twitter card meta tags point to
https://devdle.iamdevloper.io/og.pngfor consistent previews. - Rate limiting DO protects from accidental firehoses (or excited players).
- Cron jobs are idempotent; re-generating a day will just update KV + D1 gracefully.
Things Learned the Hard Way
- Validate everything – Zod schemas catch OpenAI drift. Single-output puzzles now auto-convert; no more failures just because the model picked the wrong type.
- Limit the surface area – Three languages, one puzzle type. Fancy variety can wait until I'm comfortable with the base game.
- Telemetry early – GA events for
first_attempt+guesslive from day one so daily active players are obvious. - Edge infra is friendly – KV/D1/Durable Objects cover caching, storage, and rate limiting without pet servers or cron boxes.
- Make puzzles answerable – Order mode got a proper UI, sequence mode gets hints, options are always six clickable tiles.
Next Steps?
- Grow the puzzle backlog, add explainers for multi-choice answers.
- Explore optional leaderboards or streak sharing once base DAU is steady.
- Extend analytics with puzzle difficulty/time-to-solve metrics without creeping on the "no login" promise.
For now, Devdle does exactly what I wanted: a five-minute daily diversion for the dev brain, low-maintenance to run, and easy to share.