Skip to content
How it is built

One puzzle for the whole world, no database

By Ari Hlynsson ·

Everybody gets the same daily board and nothing is stored anywhere. Two functions turn today's date into a seed, and each browser generates the puzzle for itself.

Four games here have a daily board: Word of the Day, Sudoku, Solitaire and Anagrams. Everybody who plays today's Sudoku gets the same grid. Yesterday's players also shared a grid, and theirs was different from today's.

The usual way to build that is a table of puzzles with a date column, a scheduled job to fill it, and an endpoint to serve today's row. Instead, each browser generates the board for itself and no board is stored anywhere.

The mechanism

Two functions do the work. The first turns today's date into a number, counting days from a fixed epoch of 1 January 2024:

export function dailyIndex(now: Date = new Date()): number {
  const localMidnight = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
  return Math.floor((localMidnight - EPOCH) / DAY_MS);
}

The second turns a game and a day number into a seed:

export function dailySeed(gameId: string, day: number): number {
  return hashString(`${gameId}#${day}`);
}

That seed drives the same deterministic generator the game already uses for a random board. sudoku#981 hashes to one number on my laptop in Reykjavík and to the same number on a phone in Seoul, because hashing a string does not vary by machine.

Each browser therefore produces the same board from the same seed and the same code, without asking anything for it.

Why the shape suits a site with no money

I am one person paying for hosting out of pocket, and this approach removes several running costs at once.

Adding a fifth daily game needs no database migration. There is no scheduled job to fail quietly overnight and leave tomorrow without a puzzle, and no endpoint whose traffic grows with the number of players. The date and seed calculation runs in the browser, so the daily board cannot go down while the rest of the site stays up.

It also keeps working offline, which I did not plan and noticed later. A player who loses signal in the middle of a puzzle carries on.

The streak is the reason it exists

I added daily puzzles to give people a reason to come back. This site has no notifications, no email list and no app, so a board that changes every day with a count beside it is the only thing here that builds a habit. For a site funded by advertising, one person returning on eighty days is worth considerably more than eighty people arriving once each.

Progress lives in localStorage, so it survives a closed tab and does not survive a cleared cache. Signing in is optional and changes only how long the record lasts. The account sync merges the same localStorage keys up to the server and back down, which means no game has to know whether a server exists. Every game reads its streak from the place it always did.

A choice I made on purpose

dailyIndex uses the visitor's local calendar date rather than UTC, so the day boundary moves with the player. When it is Tuesday in Auckland and still Monday in Reykjavík, those two players are working on different puzzle numbers for about half a day.

Using UTC for everybody would mean a genuinely simultaneous race, at the price of a reset at midnight UTC. That lands at lunchtime in Auckland and in the evening in Los Angeles.

I chose local time because a daily puzzle is a morning habit, and a habit needs to reset while you are asleep. Racing strangers at the same instant matters less than tomorrow's puzzle being there when you wake up. The practical effect is that a given day's leaderboard fills over roughly 36 hours rather than 24, and that has not caused a problem worth fixing.

What the approach does not survive

Storing nothing works only while the generator stays fixed. Change how a puzzle is produced from a seed and every historical day changes with it. Day 981 is a recipe rather than a record, so editing the recipe quietly rewrites the past.

If the archive ever has to be accurate, the fix is to start storing boards from that day forward and leave the older ones derived. I have not needed it so far.

More on how it works

  • The chat box I did not build

    Eight preset phrases, sent as an index rather than as text, so free chat is unrepresentable on the wire rather than merely refused.

  • The friend list my server never sees

    A green dot telling you which friends are online, drawn by a server with no database connection and no way to learn who your friends are.

Play a game instead