Implementation Note
July 13, 2026 · 4 min read
The first build: Elo, atomic writes, and logo cleanup
I kept the MVP stack simple: Next.js Server Actions, Supabase Postgres, and a small Elo engine with careful vote writes.
For the first real build, I tried to keep the backend compact enough that I could still understand every moving part. Next.js Server Actions fetch matchups and record votes. Supabase Postgres stores company rows, internship metadata, current Elo ratings, and anonymous analytics events.
The most important implementation choice was the vote write path. I did not want a vote to be two loose update calls from the browser. The server calls one Postgres function, `record_company_vote`, and that function locks both company rows before calculating the new ratings. That means concurrent votes do not quietly overwrite each other.
The ranking algorithm is Elo. I liked it here because the formula is small enough to understand, but it still gives each matchup context. If a lower rated company beats a higher rated one, the rating move is larger. If the expected favorite wins, the move is smaller.
E = 1 / (1 + 10^((R_other - R_old) / 400))
R_new = R_old + K(S - E)
For example, if two companies both start at 1200, the expected score is 0.5 for each side. If one wins, its new rating becomes 1200 + 32(1 - 0.5), which is 1216. The loser becomes 1200 + 32(0 - 0.5), which is 1184. That small change is enough to move the leaderboard without letting one vote completely dominate it.
I also moved the company data into `data/internships.csv`. Each row carries the company name, domain, hourly pay, report count, location text, and a short internship detail. The seed script handles cleanup too, like removing companies I do not want in the app and overriding domains when logo lookup needs help.
The logo system took more tinkering than I expected. I started with remote logo links, then moved to domain-based logo.dev images. Now the frontend asks for a logo based on the company domain. If a company needs a different logo domain or a darker background, I store those overrides in Supabase. If the logo still fails, the UI falls back to a clean initial badge instead of showing a broken image.
The thing I am learning is that simple architecture is not the same thing as careless architecture. This app is still small, but vote writes are atomic, logos fail gracefully, and the data pipeline can be rerun without hand-editing hundreds of companies. That is the kind of boring reliability I want to get better at building.