TapDrop is a tap-to-win web game where a win hands over a real partner voucher, so the odds, the stock and the leaderboard all have to hold up against retries, races and scripts.
Role
Full-stack web app
Platforms
Web
Stack
Next.js, Drizzle, PostgreSQL, Vercel
Try it
The problem
Real prizes, untrusted clients
Most tap games can afford a bug, because the reward is a number on a screen. In TapDrop a win hands over a partner brand's voucher, and each code exists once. Two players cannot get the same code, and a retry cannot pay twice.
The traffic is awkward too. Taps arrive in bursts from phones on bad connections, across several server instances, and some come from scripts. The system has to answer quickly, count each tap once and catch abuse without punishing honest players.
Architecture
One transaction per batch of taps
I put the game rules in a small package that takes time, randomness and config as inputs and never reads a clock itself. A Postgres adapter runs each batch of taps as one transaction holding the player's row lock, so everything one player does is serialised across instances. Each tap carries its own id, so a retry gets its stored answer back and spends nothing.
The win chance is one number from the running game config. Spending TAP in the shop does not enter the roll, and levels only change energy, so the chance per tap stays the same for everyone.
Client
Next.js, React
Queues taps, sends batches, retries with the same ids
API
Next.js on Vercel
Validates the batch and resolves the player
Game rules
Tap engine package
Energy, drops, win roll and voucher allocation
Storage
PostgreSQL, Drizzle
One transaction per batch under a row lock
Anti-cheat
Anti-cheat package
Reads outbox events, scores players, raises flags
Scoring runs after the response is sent. The tap transaction only reads the stored result.
Key decisions
Where a bug would cost vouchers
01
Anti-cheat outside the tap path
Analysing rhythm, devices and sign-ups inside every tap would slow the busiest path. The signals are also noisy, so one crude rule would punish honest players.
The game writes facts to an outbox table in the same transaction as the change they describe, so a rolled-back tap is never scored. A separate engine runs 13 detectors from one registry, folds events into its own schema and stores a score, and the tap transaction reads only that result. A flagged player's wins are held for review rather than refused, and the win roll itself is never touched.
Signals a browser reports count as weak, and their total is capped.
The engine has its own schema and writes to no game table.
It stores only hashed IPs and drops any other field.
02
One voucher, one winner, no waiting
Many players can win from the same campaign at the same moment. A plain row lock would queue them behind each other, and a read followed by a write could hand one code to two people.
When a campaign's pool is locked, its vouchers are shuffled once and given a fixed draw position and release time. A win runs one UPDATE that picks the next released voucher with FOR UPDATE SKIP LOCKED, so concurrent winners take different rows without blocking. Database constraints back this up: one tap cannot own two vouchers, and a position cannot repeat within a campaign.
The shuffle is a Fisher-Yates driven by Web Crypto.
Release times can spread stock evenly across a campaign's window.
A Postgres test races two winners and expects different vouchers.
03
Weekly prizes that pay exactly once
The daily cron can run late or be missed, and a leaderboard week still has to close and pay. Two instances settling the same week must never pay it twice.
Both the cron and the first request after a week closes can trigger settlement. One conditional UPDATE sets the settled time inside the transaction that writes the awards, so only one caller ever gets to pay. Each period reserves its voucher prizes when it opens, so the board only advertises prizes it holds.
Settlement waits 60 seconds after the week ends, so in-flight taps land first.
A prize voucher that is rejected passes to the next eligible player.
A prize's TAP counts for the week it was won, not the week it is claimed.