Dev Puzzle

Behind the Scenes: How Dev Puzzle Picks a New Word Every Day

Behind the Scenes: How Dev Puzzle Picks a New Word Every Day

One of the smallest features in Dev Puzzle, the bit that picks today's word, is also one of the most fun pieces of infrastructure in the project. In this post I want to walk through how it actually works under the hood: the data model, the scheduled job, and the small handful of mistakes I made on the way.

The requirements

Before any code, the constraints:

  1. One word per difficulty per day. Easy, Medium, and Hard all roll over at midnight.
  2. Same word for everyone. If a friend on the other side of the world plays at 2pm UTC, we should match.
  3. No repeats within a reasonable window. Nobody wants RECURSION two Tuesdays in a row.
  4. Cheap to run. This is a free, ad-supported game; the puzzle generator should not be a budget item.

The data model

In Firestore, the puzzle data lives in a single collection called dailyPuzzles. Each day's document is keyed by ISO date:

dailyPuzzles/
  2026-01-12: { easy: "BYTE",  medium: "COMMIT",  hard: "RECURSION" }
  2026-01-13: { easy: "LOOP",  medium: "STRING",  hard: "POLYMORPHISM" }

This means the client only needs to read a single document, by deterministic ID, to get everything it needs for the day. No queries, no indexes. The free Firestore tier handles this easily.

The scheduled job

A Firebase Cloud Function runs once per day on a Cloud Scheduler trigger. The job is dead simple:

  1. Pick today's date.
  2. For each of easy, medium, and hard, look at the curated word lists.
  3. Filter out anything used in the last 30 days (we keep a small recentWords doc with rolling history).
  4. Pick a random remaining word.
  5. Write the document.

The "filter out recent" step is what keeps it from feeling repetitive. The lists themselves are big enough (a few hundred words per difficulty) that 30 days of cooldown still leaves plenty of variety.

The client read path

When you load /puzzle in the browser, the client computes today's date as a YYYY-MM-DD string, then does a single getDoc on dailyPuzzles/<that-date>. If the doc exists, great, render the puzzle. If it doesn't (unlikely, but the scheduled job did fail once early on), the UI shows a friendly error and a "try again later" message.

I considered moving the puzzle word into a callable Cloud Function so the answer never sits in the client. But the answer is going to end up in the browser anyway by the time you finish typing, and the simpler architecture won.

The mistake I made early on

The first version generated the day's word on the client using a seeded RNG keyed off the date. It worked, it was clever, and it was wrong: as soon as you played in two timezones, you got two different "today's" words. A user on the East Coast and a user in Tokyo were not actually playing the same puzzle.

Moving the source of truth to Firestore fixed it cleanly. The lesson, as always, is that "current date" is a server concern, not a client one.

What's next

A few things on my list:

If you'd like to suggest a feature or just say hi, the about page has a contact email. Otherwise, try today's puzzle and see what the scheduled job picked for you.

← Back to the devlog