Building Devdle: Notes from the Neon Arcade

15 Oct 2025
Ben Howdle

Ben Howdle

Product Engineer
Helping startups build and scale

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

  1. One fresh coding puzzle per UTC day.
  2. Zero login, zero friction, works on a phone.
  3. 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

  1. Player hits the SPA → initAnalytics()trackEvent('app_open').
  2. App calls GET /api/today.
  3. Worker tries:
    • KV cache (12h TTL),
    • otherwise D1 lookup,
    • otherwise generate new puzzle.
  4. 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.
  5. Client hashes guesses (SHA-256(answer|salt), truncated 16 chars) to compare locally. Salt is public but rotated annually.
  6. 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 /today read 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.
  • SecretsVITE_API_ORIGIN, OPENAI_API_KEY, DAY_PUBLIC_SALT, SECRET_SALT managed 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_attempt GA 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.tsx uses 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 repeating array-filter-reduce.
  • Single-answer enforcementenforceOutputPuzzle now converts accidental output puzzles 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.png for 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

  1. Validate everything – Zod schemas catch OpenAI drift. Single-output puzzles now auto-convert; no more failures just because the model picked the wrong type.
  2. Limit the surface area – Three languages, one puzzle type. Fancy variety can wait until I'm comfortable with the base game.
  3. Telemetry early – GA events for first_attempt + guess live from day one so daily active players are obvious.
  4. Edge infra is friendly – KV/D1/Durable Objects cover caching, storage, and rate limiting without pet servers or cron boxes.
  5. 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.

Enjoyed this post?

Let's work together to accelerate your product development.