Liga dos Vales
Official portal for a regional volleyball league · my own project · 192 commits over 6 months
Context
The public site and the back office for the largest volleyball championship in Santa Cruz do Sul, Brazil: stages, standings, playoff brackets, a player transfer market, per-stage awards and a news section. The league's organisers run it themselves.
What makes this one interesting is that the rules are not mine to invent. Scoring and tie-breaks come from a printed regulation, and the code cites its articles. Getting them wrong does not raise an error — it publishes a wrong ranking, which the teams read before I do.
Engineering decisions
- 01
Award three points for a win and none for a loss, not the federation's 3/2/1/0
WhyThe league's own regulation says so, in articles 24a and 30. The international scale is the obvious default and it is the wrong one here.
What it costI shipped the obvious default first. It mis-ranked teams silently until I read the rulebook line by line against the code — the kind of bug that has no stack trace and no failing test, only a standings table that looks plausible.
- 02
Rank across groups by point average, never by raw points
WhyThe groups are not the same size — four, three and three. Raw totals are not comparable between them, so a cross-group table built on points would reward whoever played more matches.
What it costAn extra path through the tie-break code, and a comment warning whoever comes next not to 'fix' it back to points. It reads like an inconsistency until you know why it is there.
- 03
Cache-bust generated share images with a version derived from the data
WhyThe images are generated and cached, but they have to change the moment a result changes. A fixed URL serves a stale scoreboard; a random one throws the cache away.
What it costA version query on every image request, and a window of up to an hour in which a very recent edit may still serve the previous image.
- 04
Call the authorisation check inside every server action instead of relying on route middleware
WhyServer actions are globally reachable POST endpoints. The middleware that guards the admin URLs never sees them — a fact that is easy to learn the wrong way.
What it costThirty-eight call sites to get right, and no structural guarantee that the thirty-ninth will have it. The check is a convention held by hand, which is exactly what the middleware was supposed to avoid.
- 05
Make a transfer an atomic conditional update rather than a read-then-write
WhyTwo people submitting a transfer for the same player at the same time would otherwise corrupt which team he belongs to, and the loser would never know.
What it costTranslating the database's constraint violation into a message a human can act on — the error is precise and unreadable, and the person filling the form deserves the opposite.
What broke, and what I learned
A security review found the back office was not the boundary I thought it was
What happenedThe middleware guarded the admin URLs, but server actions are imported into public pages and reachable directly — around thirty-eight data-changing actions could be invoked with no authentication at all. A database seeding route had also been left open, and the admin credentials existed as literal defaults in the source.
The fixAn authorisation check inside every action, field allowlists on everything serialised into public pages, the seeding route removed, and the password replaced by a hash in an environment variable with no fallback — because a silent default is worse than a loud failure: it works, and so nobody finds it.
Regenerating a bracket deleted matches that had already been played
What happened'Generate bracket' deleted and recreated the knockout matches. In the ten-team format, with a play-in feeding a second semifinal, three finished results could vanish from one click in the middle of the tournament — and the button gave no warning that it would.
The fixQuery for finished playoff matches first and return a confirmation step that names the exact scores about to be lost. The destructive action is still there; it just can no longer be taken by accident.
Sharing a result to Instagram failed silently on phones
What happenedThe browser's share API rejected on some mobile devices and the app had no way to know why. The user tapped, nothing happened, and there was nothing to debug from — the worst shape a bug can take, because it is invisible to everyone except the person it happens to.
The fixInstrument the three failure points to report the error name and whether file sharing was supported at all, then use that telemetry to add a fallback that shares without the image, and fix a stale closure that left the button hanging. Measuring first turned an unreproducible complaint into two specific bugs.