I wanted to put something on the Virya website that people could enter physically. Not another gallery, not another "click here to stream the album" button, but a small game living somewhere between the phone and the city.
That became VIRYA AREA. A signal appears in a city, the player goes to the location, opens the page and tries to unlock one of a limited number of rewards. No native application, no QR sticker and no NFC tag. The browser is the interface.
The phone is not proof
At first this sounds almost too easy: ask the Geolocation API for latitude and longitude, calculate the distance and hand out the prize. The problem is that a browser gives me a reading, not proof. The position may be old, inaccurate, affected by buildings or simply spoofed. A phone can honestly believe it is somewhere else, and a dishonest client can say anything it wants.
Friendly frontend, suspicious backend
So the frontend does the friendly part. It asks for permission, collects readings, shows whether accuracy is improving and tells the player that the signal is close. The server makes every decision: is the drop active, is the challenge fresh, was it already used, is the reading recent enough, is the accuracy acceptable, is the player inside the radius, is there still capacity and has this person already claimed it?
Do not ship the answer
I also stopped shipping exact live coordinates in the JavaScript bundle. Hiding them does not make GPS impossible to fake — a browser game will never give me that guarantee — but at least the easiest solution is no longer "open DevTools and read the answer".
The last reward is a race condition
The last available reward created another nice trap. Two phones can pass the distance check at the same time and both see one slot left. The claim therefore became a reservation committed together with the wallet entry, protected by an idempotency key. A retry returns the same result instead of creating another voucher.
Checkout is not redemption
The voucher itself follows the same logic. Opening Stripe checkout does not consume it. Payment may fail or the person may close the tab. The reward is reserved when checkout begins and consumed only after the Stripe webhook confirms the order.
Enough data to decide, not to track
AREA does not store a route history. I need the readings used for the current decision, their accuracy and the outcome — not a map of where somebody spent the afternoon. The audit trail explains why a claim passed or failed without quietly turning the game into a tracking system.
The result is a small browser game with a suspicious number of backend problems hiding underneath: untrusted clients, expiring challenges, concurrent limits, retries, partial failures and webhook-driven state. In other words, exactly the kind of fun I apparently choose for my free time. :)
