The full engineering deep-dive: every real piece of the system, the same names used throughout this site, and the tradeoffs behind each decision.
Tourney's own box (AWS): Start with the one thing everything else sits on top of: a single AWS server, running the whole app in Docker — no server fleet, no managed cloud services to wire together, a deliberately lean footprint sized for a free pool with a modest number of players. On that one box, three containers do the work:
gunicorn (Tourney App): gunicorn is the web server that runs the actual Tourney application and handles real traffic reliably. The app sits in the middle, and almost everything it does is a call out to somewhere else:
Spark: an NVIDIA DGX Spark — a dedicated GPU box you load your own models onto, sitting on the developer's own private network, not a vendor (not to be confused with the unrelated Apache Spark data-processing framework). It runs a self-hosted, open-weight model (gpt-oss:20b, served by Ollama) on that hardware. It isn't just a second API call: it runs code we built and operate ourselves, including an agentic loop that lets the model ask for its own data mid-run. A worker process on that same box watches its own job queue and picks up work whenever there's any — and that's also what keeps it safe: the Spark never accepts an inbound connection and never touches our database directly, so the only way anything reaches it is by asking, never by being reached.
Data Warehouse: Behind the live app, a second, quieter path exists purely to learn from the data rather than serve it: an ETL script pulls a conformed, offline copy of teams, players, games, and odds into a DuckDB data warehouse — kept deliberately separate from production so analysis never touches live scoring, feeding the search for what actually predicts a good draft pick (full detail on Data Warehouse).
Two things worth knowing for planning purposes, each already accounted for:
App & hosting
Frontend
Data sources
AI
Email & analytics
Quality & CI/CD
One straight line from commit to production, through three environments in order — no side branches. A commit to main auto-rebuilds SIT the instant its 5 gates (secrets · tests · lint · bandit · pip-audit) go green; the one human step is testing in SIT and clicking "Run workflow"; from there GitHub Actions promotes sit straight to prod, fast-forward-only.
What each environment holds and its worst case if compromised is on The Environments; full detail on the gates and the promotion mechanics is on Deploy; this is just the shape of it.
prod can move straight to sit's exact tip — GitHub Actions' fast-forward-only promotion, covered in full on Deploy.Two boxes, two machines. Tourney's own box is wherever the web app and its MySQL happen to be running — a dev laptop, SIT, or production — and that same MySQL holds the job queue, so "dropping a job in the queue" is really just a row in the app's own database. The Spark's own box is separate, physical, private hardware: it holds a worker process and the open-weight model itself. The app calls Claude directly, over the open internet, the same as any other API call. For the open-weight model, the app never calls out at all — it drops a job in the queue and the Spark's worker comes and gets it on its own schedule, so the Spark is never the one being reached into. (A second, queue-free way to reach the Spark directly also exists in the code, over a private Tailscale network, but it's currently switched off while that network path gets revisited — not shown here.) Full detail on both, including exactly how the queue path is secured despite crossing the open internet, on AI: Foundation vs. Open-Weight.
gpt-oss:20b locally, on the Spark — a call to Ollama that never leaves that machine.Solid arrows are one machine calling another directly. Dashed arrows are the Spark's own worker reaching out on its own — the Spark accepts no incoming connections, so every dashed arrow starts from the worker, never the app.
For an agentic run, steps 2 and 4 each still happen only once — claiming the job, and posting the finished commentary, are separate calls that don't repeat. What repeats is a third kind of call, sandwiched inside step 3: every time the model wants data (today's standings, results, top movers) instead of finishing, the worker posts that one specific ask to the app, gets the answer back in the same response, and feeds it to the model before continuing — up to 20 times per job. Same pattern as steps 2/4 (worker calls out, app answers, nothing ever calls back into the Spark), just its own separate exchange. The Claude-direct agentic arm works differently and never touches the Spark at all: its tool loop runs entirely in-process inside Tourney's own box, calling the same Python functions directly rather than crossing any network.
"Tourney's own box" is really three different boxes over time, all hitting this same one Spark. There's only one Spark in the whole system, not a copy per environment — a dev laptop, SIT, and production (AWS) each write their jobs into their own queue, and the same physical Spark reaches out to check all three in turn. SIT is the odd one out: it doesn't even cross a network to get there, since SIT's own deploy lives directly on the Spark's own disk — dev and production reach it over a hop, SIT is already there. Full detail on how that's wired on The Environments.
Steps 2 and 4 above are the two moments a request actually crosses the network, and both cross it the same secured way: mutual TLS, the same encrypted-connection technology behind any https:// address, extended so both sides prove who they are rather than just one. Every time the Spark's worker reaches out, it presents a certificate — a private credential belonging to it and to no one else — and the app only continues once that certificate checks out.
A certificate only works once two things have already happened, each done once and by hand: it's created for that one worker, and it's separately registered on that environment's own approved list. A certificate that's genuine but was never registered — or one issued for a different environment entirely — is refused before the request ever reaches the app itself. Because dev, SIT, and production each keep their own certificate and their own approved list, a credential from one can never be used against another.
The full diagrams — every real piece, the same names used throughout this site, not simplified stand-ins — are in Big Picture and AI above, and on the dedicated Special Features page, which goes deeper still: how ESPN scores become standings, the visual bracket report, predictive scoring, and the reasoning behind decisions like skipping a traditional database backup. One thing worth calling out here: the job queue is how the app reaches the open-weight model without ever calling out to the Spark itself — the app writes a pending job row, spark_worker.py polls it out over that same secured connection rather than the app calling in, and the worker calls Ollama locally before posting the result back. The table below is the click-through map: every named piece links to where it's covered in full engineering depth.
| Component | What it is | Read more |
|---|---|---|
| Flask web app, MySQL, AWS EC2 | The one-server hosting setup: how it's packaged, deployed, and kept running | Deploy → |
| ESPN public APIs | The real, live source for the tournament field, rosters, and every score | Special Features → |
| Claude & the Spark | The same daily-commentary task, run through a hosted model and a self-hosted open-weight model, side by side | AI: Foundation vs. Open-Weight → |
| Amazon SES | Sends account emails such as password resets | Player Experience → |
| Analytics warehouse (DuckDB) | A separate, offline copy of the data used to study what predicts a good draft pick — deliberately kept apart from live scoring | Data Warehouse → |
2 of 5