DEV Community

makuna hatata
makuna hatata

Posted on

How I built a live global scoreboard for $0 using Static HTML and Supabase published: true

I wanted to see how far I could push a $0 zero-backend stack without spinning up a server, paying for hosting, or melting a free-tier database during a traffic spike.

The result is Tap The World β€” a single shared global scoreboard where every visitor on Earth gets ONE tap, attributed to their country in real time via IP geolocation.

Tap The World Banner

πŸ”΄ Try the Live Demo: taptheworld.netlify.app


The Stack ($0 Total Cost)

  • Frontend: One static index.html file (HTML, CSS, vanilla JS)
  • Database: Supabase (PostgreSQL on the free tier)
  • Hosting: Netlify (static edge deployment)
  • Geolocation: ipwho.is (free client-side IP lookup, CORS-enabled)

Engineering for Zero Servers & Free-Tier Limits

When building a live public counter, the default instinct is to log an event for every tap and use WebSockets to push live updates. On a free tier, that will kill your database in minutes.

Here is how I designed around those constraints:

1. One Row Per Country (Not One Row Per Tap)

Instead of inserting an event row per click, the PostgreSQL table is structured as:

create table country_counts (
  country_code text primary key,
  country_name text,
  count bigint default 0
);
Enter fullscreen mode Exit fullscreen mode

The database table stays under ~200 rows forever. Whether the site gets 10 taps or 10,000 taps, query performance and storage remain completely flat.

2. Polling Instead of Realtime WebSockets

Holding open a persistent WebSocket connection for every concurrent visitor drains Supabase free-tier connection limits rapidly.

Instead, the frontend polls SELECT * FROM country_counts every 2 seconds. For a dark split-flap departure board aesthetic, a 2-second mechanical tick feels natural and keeps server load predictable.

3. Locking Down Writes with security definer

Since there is no backend server, the browser talks directly to Supabase. To prevent anyone from opening the browser console and sending UPDATE country_counts SET count = 999999, I revoked direct INSERT and UPDATE permissions on the table for anonymous users.

All taps pass through a custom PostgreSQL function:

create or replace function tap(p_country_code text, p_country_name text)
returns bigint
language plpgsql
security definer
as $$
-- Secure upsert logic increments the country count atomically
$$;
Enter fullscreen mode Exit fullscreen mode

The public API key only has EXECUTE permission on tap(), making schema corruption impossible from the REST API.


What’s Next?

I’m currently exploring lightweight edge-rate-limiting to supplement the client-side localStorage spam check without adding backend server costs.

I'd love your feedback on:

  1. The PostgreSQL schema and function design.
  2. How you handle rate-limiting on pure static/serverless setups!

Drop a tap for your country and let me know what you think in the comments! πŸ‘‡

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.