DEV Community

Saya Mori
Saya Mori

Posted on

Deterministic Daily Draws: Same Card All Day, No Database

A "card of the day" feature has one requirement that sounds trivial and is not: the same user must get the same card all day, and a different one tomorrow. The obvious implementations are all worse than they look.

Why storing it is the wrong default

Store (user_id, date) -> card and you have solved it. You have also acquired a write on every first-visit-of-day, a table that grows unboundedly, a timezone column, and a cleanup job.

For a feature whose entire output is one card, that is a lot of infrastructure. The alternative is to make the draw a pure function of inputs you already have.

Seeded hashing

function dailyCard(userId: string, date: string, deck: Card[]): Card {
  const seed = hash32(`${userId}:${date}`)
  return deck[seed % deck.length]
}
Enter fullscreen mode Exit fullscreen mode

Same user, same date, same deck -> same card, every time, on any server, with zero storage. Different date -> different hash -> different card.

Two things that matter:

Use a real hash. Math.random() seeded by string concatenation is not reproducible across engines. FNV-1a or xxhash are small, fast, and stable — stability across runtimes is the whole point.

Modulo bias is negligible here but real. With a 32-bit hash and a 78-card deck, the bias is far below perceptibility. Worth knowing it exists before someone asks.

Timezone is where this actually gets hard

"Today" is not a global fact. A user in Tokyo and one in Los Angeles disagree about the current date for several hours daily.

Options, none free:

  • UTC for everyone — simple, but the card flips mid-afternoon for some users, which feels broken
  • Client-reported local date — correct-feeling, but the client can lie and re-roll by changing its clock
  • Server-side timezone per user — correct, but now you are storing user state, which was the thing you avoided

What I settled on: client-reported date, server-validated to be within ±1 day of UTC. Someone determined can still re-roll — and honestly, if a user wants to change their system clock to get a different tarot card, letting them is fine. Match the strictness to the stakes.

Deck versioning

If you add cards, seed % deck.length shifts and everyone's card changes retroactively. Anyone who screenshotted today's card now sees a mismatch.

Fix: version the deck and include the version in the seed. Old versions stay stable, new draws use the new deck.

const seed = hash32(`${userId}:${date}:${deckVersion}`)
Enter fullscreen mode Exit fullscreen mode

Cheap to add up front, painful to retrofit.

I use this pattern in Oraclely — one card each morning across tarot, four-pillar, and Western astrology, no per-user draw storage.

Where it does not apply

If you need draw history — "show me my last 30 cards" — you need storage anyway, and the pure-function approach only saves you the write path. It shines when the draw is genuinely ephemeral.

Top comments (0)