Leaderboard · Writing

Rank is never stored: how this board is built

An append-only ledger, derived ranks, and the queries that fall out of it for free.

A pay-to-rank board looks like a weekend project, and the naive version is. The interesting decision is the one that determines everything else: whether rank is a column you write or a result you derive. We derive it, and this is what that buys.

There is no rank column

The obvious schema gives each listing a rank and a total. Both are traps.

  • A stored rank has to be rewritten across every row below the bidder on every single payment. One $500 bid on a busy board is thousands of writes, in a transaction, while people are still bidding.
  • A stored total destroys the information needed for a rolling board. Once you collapse payments into a running sum, when each dollar arrived is gone, and a "last 24 hours" ranking becomes impossible to compute.

So bids is an append-only ledger — never updated, never deleted — and every board on the site is one query shape over it:

sum the bid ledger
  -> order by money desc, then by who got there first
BoardQuery
All-timeSUM(amount_cents) GROUP BY listing
TodayThe same, plus WHERE created_at > now() - interval '24 hours'
CategoryThe same, filtered before ROW_NUMBER() so ranking happens inside the category

That last detail is the one people get wrong. Filter after ranking and a category page shows listings numbered 4, 9, 17 — their positions on the global board. Filter before, and a listing sitting 40th overall correctly leads the category it competes in.

Raising a bid needed no code

"Raise your bid, pay only the difference" sounds like a feature with edge cases: partial payments, refunds, what happens if two raises race. With a ledger it is not a feature at all. A listing's bid is the sum of its payments, so charging the gap and appending one row lands on exactly the right total. There is no special case, because there is nothing to keep consistent.

This is the recurring payoff of deriving rather than storing: the awkward requirements stop being requirements. Ties are the same story — ORDER BY total DESC, first_bid_at ASC gives "the older listing keeps the higher rank" for free.

The rules cannot drift from the code

Every bidding rule is a pure function in one module, and the public rules page renders from the same constants those functions enforce. Change the minimum bid and the published rules change with it. On a site taking money, the failure mode this prevents — documented rules that quietly stopped matching the behaviour — is the one that turns a disagreement into a chargeback.

Listing identity is a normalisation problem

Two people bidding on "the same" thing must land on one listing, or the board splits and both bids under-rank. So URLs collapse to a stable key: www. and query strings are stripped, @handle and x.com/handle resolve to one entry, and platform links — App Store, Play Store, GitHub — are keyed by path so two repositories do not merge into one listing.

Postgres in development, Postgres in production

With no DATABASE_URL, the app runs on PGlite: Postgres compiled to WebAssembly, stored on disk. Point the variable at a hosted Postgres and it switches drivers with no code change and the same migrations.

That matters specifically because of the ranking. It leans on CTEs and window functions, and a SQLite-shaped stand-in would diverge from production in exactly the queries that decide who is at #1. Tests run against a real Postgres engine in memory, so they exercise the statements that actually ship.

Was it worth building?

Honestly: as a business, being clone number one-hundred-and-something is a poor position, and we say so on the alternatives page. The traffic curve that made the original work is not available to anything launched after it.

As an exercise in one decision — derive, do not store — it paid for itself repeatedly, and the rolling board, the category rankings and the free raise mechanic are all consequences of that single choice rather than features anyone had to design.