Start a project
Case study / 05

Zeintzeonk Nagusi?

A king-of-the-hill game on the map of the Basque Country. Pick your town, pay the last bid plus one euro, and your name and message stay on it until someone pays more. Next.js, Stripe, Postgres and Realtime, from idea to production in one day.

  • Own product
  • 2026
  • Web
  • Role: everything
  • Status: live
Zeintzeonk Nagusi?: the map of the 681 Basque towns, coloured by price
01
Zeintzeonk Nagusi?: Azpeitia selected on the map, with its current boss and message
02
Zeintzeonk Nagusi?: live bids, the most expensive towns, the top bosses and progress by territory
03
01 — ContextProduct

"Zeintzeonk nagusi?" is Basque for "who is the boss around here?". The game takes the question literally: Araba, Bizkaia, Gipuzkoa, Nafarroa and Iparralde, 681 towns, and each one belongs to whoever paid last. There is nothing to win but the title and eighty characters of message, and that is the point: local pride and a bit of mischief between neighbouring towns.

It is the fifth ZubiLabs product and the first one for the web. It was also a speed test: from an empty Next.js project to a live site with real payments, SEO and share images in a single working day.

02 — ProblemProduct

The rules fit in one line, but money makes them strict. Two people can pay for the same town in the same second and only one of them can win. The other has paid for nothing and must get the money back without writing an email. A payment Stripe confirms twice must not count twice.

It is also a public wall. Names and messages appear on a map anyone can share, so links, insults and invisible characters have to be stopped before the payment, not after. And the whole thing had to be in Basque, including 681 town names that official datasets mostly give in Spanish or French.

03 — SolutionProduct

One page with the map at the top. Each town is coloured by its price, from free to 500 € and more; tapping one opens its card with the current boss, the message, how long they have held it and the minimum bid. Below the map: search across the 681 towns, a live feed of bids, the most expensive towns, the bosses with the most towns and the progress of each territory.

Bidding goes through Stripe Checkout. The server validates the name, the message and the amount and opens a session; the webhook applies the bid once the payment is confirmed, and every open map updates through Supabase Realtime. Each town has its own page, its own share image and a vertical 1080×1920 image for Instagram and WhatsApp stories, so every purchase is also an invitation to the town next door.

04 — Role

Everything. The game rules, the geographic data and its cleanup, design and Basque copy, the Next.js app, the database schema and the bid function, Stripe with automatic refunds, moderation, realtime, SEO with a page per town, share images, the legal pages and the deploy.

05 — ArchitectureEngineering
MAPMapLibre · React 19ROUTE HANDLERSNext.js 16 · VercelPOSTGRESSupabase · RLSGEOJSON · 681 TOWNSSTRIPE CHECKOUTWEBHOOK · REFUNDSUPABASE REALTIMEOG · STORY IMAGESpuja_egin · ROW LOCK

Key decisions, and why:

  • The bid is one Postgres function. puja_egin checks whether the Stripe session was already processed, locks the town row with SELECT … FOR UPDATE, compares the amount with the current price and writes the town and the bid in the same transaction. Two simultaneous payments are serialised by the database; there is no race left for the application code.
  • The webhook is the only writer. The browser never writes a bid. The checkout route only opens a session; the bid is applied when Stripe confirms the payment, delayed methods included. If the function answers that someone paid more in the meantime, the webhook refunds the payment on the spot.
  • Read-only from the browser. Row Level Security lets anyone read towns and bids and nobody write them; writes use the secret key, on the server only. The publishable key in the page is enough for the map and Realtime, and useless for anything else.
  • Static geography. Town limits come from IGN/INE for the south and geo.api.gouv.fr for Iparralde, simplified with mapshaper into GeoJSON served as a static file, with Basque names from Wikidata. The database holds only what changes: price, boss and message.
06 — Technology
WebNext.js 16 · App Router · React 19 · TypeScript · MapLibre GL
DataSupabase Postgres · Row Level Security · Realtime · Presence
PaymentsStripe Checkout · Webhooks · Automatic refunds
SEO681 town pages · Sitemap · JSON-LD · next/og images
GeoIGN / INE · geo.api.gouv.fr · Wikidata · mapshaper
OpsVercel · Demo mode without keys
07 — ChallengesEngineering
  • Two winners for one town. Stripe confirms payments asynchronously and can deliver the same event twice. The bid is idempotent on the session id, the row lock decides who arrived first and the loser is refunded automatically. The rules page says so in plain words, so nobody is surprised.
  • A wall anyone can write on. Names and messages are stripped of control and bidirectional characters, capped at 30 and 80 characters, checked for links and matched against a word list after normalising accents and leetspeak. All of it runs before Stripe is called, so nobody pays for a message that will be rejected.
  • Same name, two towns. Some towns share a name across territories. Slugs are built from the Basque name and get the territory appended only on a collision, so every town has a short, stable and readable URL for sharing and for the sitemap.
08 — Result

A live site with the whole game: the map of 681 towns, bids with real payments and automatic refunds, realtime updates with a five-second fallback, a page and share images per town, story images, leaderboards, a visitor counter, the legal pages and a demo mode that runs without any keys.

For ZubiLabs it is the proof of speed: a product with payments, a database with concurrency guarantees and serious SEO, in production the same day it was started.

09 — Lessons
  • Put the invariant where the data is. A bid is only valid against the price at the moment it is applied. Checking it in a database function under a row lock turned the hardest part of the product into a few lines of SQL.
  • Refund instead of preventing. Reserving a town during checkout would have meant timeouts, abandoned carts and locked towns. Letting both pay and refunding the loser is simpler for the code and fair for the player.
  • Every purchase is a share. A page, an image and a story per town turn each bid into a message to the next town. The growth loop is in the product, not in a campaign.