Tournament Manager
Pick a theme, one of the built-in ones or a set of images you upload yourself, and Tournament Manager turns it into a bracket. It shows you two entries at a time, you choose the one you prefer, and it keeps going until one is left.
The problem
Sometimes you can't decide between things, and a list doesn't help. Tournament Manager turns that into a bracket: two options at a time, you pick the one you prefer, and it advances until one is left. Useful for genuinely making up your mind, or as something to argue about with friends.
What I built
A PHP and MySQL web app for running single-elimination tournaments where you are the judge. Anywhere from 2 to 16 competitors, with odd numbers handled through byes. You can play with built-in themes or create your own and upload images to them, and every battle is recorded so the statistics page shows which entries actually win over time.
It also installs as an app, with its own icon and no browser chrome, and there's a stripped-down version that runs a bracket without an account.
Themes can also be built in bulk from the command line: a tool pulls lead images from Wikipedia articles, another crops and squares them, and a third regenerates the seed SQL.
- Bracket progression driven by AJAX against JSON endpoints, so rounds advance without a page reload.
- An Android wrapper build. The app installs from Chrome as a PWA, but Play Store verification is blocked by the host's JavaScript challenge.
Decisions & trade-offs
Rewriting the security foundations
The original built its SQL by string concatenation, kept passwords in plain text, and let anyone delete entries or inflate statistics without logging in. The rewrite uses prepared statements throughout, upgrades old passwords to bcrypt on the next login instead of forcing a reset, and asks for a session, theme ownership and a CSRF token on every write.
Validating uploads by decoding them
Uploads are validated by decoding the file, not by trusting its extension: a file that starts with the bytes GIF89a and then holds anything at all passes the usual checks. They are stored under generated names, with an .htaccess stripping PHP handlers as a second layer.
Brackets generated, not hand-written
Brackets are generated from the number of entries, replacing the eight hand-written HTML files the original had. That is what makes odd-numbered tournaments possible at all.
What I learned
I built the original mostly by hand, before I was using AI much, and going back to it now is the clearest measure I have of how much I've learned. The features worked; the foundations didn't.
The other lesson was that hosting constraints are real engineering constraints. The free host puts a JavaScript challenge in front of every request, which means curl -I reports 200 for a file that was never actually sent, and it blocks Play Store verification entirely because Chrome fetches assetlinks.json without running JavaScript. The FTP server also rejects any filename containing a space, silently truncating the upload. Neither of those is in any tutorial.
Tech stack
- PHP
- MySQL
- JavaScript
- AJAX
- Bootstrap
- PDO