DEV Community

Deterministic Daily Draws: Same Card All Day, No Database

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

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

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}`)

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.

Comments

No comments yet. Start the discussion.