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]
}
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.
Top comments (0)