Every real piece, at once

The engineering deep dive covers infrastructure, algorithms, and data model one at a time. This page is the assembled view: the full AI-routing diagram, the environments pipeline, every feature the app actually ships, and the complete tech stack behind it - the same names used everywhere else on this site, not simplified stand-ins. Zoomed all the way out, this same box is one node on the site-wide Architecture Map → - this page is Tourney's own detail, that one is how it fits alongside Runway, GetSeen, Control Tower, and the shared modules.

3 environments
dev, SIT, production - one straight line, no side branches
2 checks per cert
issued for one worker, then separately registered - both required before a connection is accepted
6 shipped features
beyond scoring itself - live scores, upsets, compare, prediction, tiebreaker, backup/restore
~25 named technologies
across hosting, frontend, data sources, AI, and CI/CD

The whole system, in one picture

The same real diagram embedded in the case study, full size and on its own page - every real node, restored from Tourney's own in-app documentation.

Browser players + admin Claude via the Anthropic API Tourney's own box dev laptop, SIT, or production nginx reverse proxy · :80 / :443 gunicorn running the Flask app — public · admin · scores · history · daily web container · python:3.11-slim calls ESPN, CollegeBasketballData, SES, and Claude directly — see right MySQL 8.0 teams · players · picks entries · scores · settings Job queue (same MySQL) scoring scheduler thread guarded — only 1 runs per host (a lock file, so a second gunicorn worker steps aside) its own always-on loop — not one HTTP request FOMX.ai Spark private hardware The FOMX.ai Spark's worker spark_worker.py gpt-oss:20b the open-weight model, via Ollama External data services Amazon SES email, via boto3 CollegeBasketballData.com betting odds & round dates ESPN public APIs site.api.espn.com datawarehouse/etl.py standalone script · not scheduled, not in Docker Analytics warehouse DuckDB scikit-learn dev-side, optional Google Looker Studio admin-triggered odds import direct call the FOMX.ai Spark's worker hitting the app's API over mTLS — never the queue directly, detail below manually run, never wired into live scoring offline copy for analysis feature profiles, trained model reporting Security tools used checked on push, or at admin login Tools ruff — lint bandit — SAST pip-audit — dependency CVEs scan_secrets.py — committed secrets Admin TOTP — MFA (pyotp)
Restored from Tourney's own in-app engineering documentation, node for node - including the animated line showing the FOMX.ai Spark's worker presenting its certificate, and the app's response coming back on its own separate line.

Two things worth knowing for planning purposes, both already accounted for. Single database, single server, by design - the right call for a free app with a modest number of players; the natural next step if that changes is splitting MySQL onto its own managed instance (Amazon RDS) so it can scale and fail over independently. The score-checking scheduler is intentionally lightweight - a built-in timer inside the website process, not a dedicated scheduling service; the upgrade path is a standalone worker with a real job queue, so scoring survives a website deploy or restart untouched.

The AI path, step by step

The same routing decision is summarized in the case study; this page has the full step-by-step, including exactly how each connection proves who's calling.

Tourney’s own box Flask app Job queue a row in the same MySQL FOMX.ai Spark - private hardware worker gpt-oss:20b via Ollama, never leaves this box 1 5 2 3 4 solid = direct call dashed = the worker polling, never the app calling in
Every dashed arrow starts from the worker, never the app - the FOMX.ai Spark accepts no incoming connections, so the only way anything reaches it is by asking, on its own schedule.
  1. Tourney drops a pending job in the queue - a row in its own MySQL, nothing more.
  2. The FOMX.ai Spark's worker polls Tourney's API and claims it - over a connection secured by a private client certificate plus a bearer token. The app never calls the FOMX.ai Spark.
  3. The worker runs gpt-oss:20b locally, on the FOMX.ai Spark - a call to Ollama that never leaves that machine.
  4. The worker posts the result back to Tourney's API - the worker initiating outward again, same as step 2.
  5. Tourney saves the result to the same queue row the job started in.

For an agentic run, steps 2 and 4 each still happen once. What repeats is a third kind of call sandwiched inside step 3: every time the model wants data instead of finishing, the worker posts that one ask to the app and feeds the answer back to the model - up to 20 times per job. The Claude-direct agentic arm never touches the FOMX.ai Spark at all; its tool loop runs in-process, calling the same Python functions directly.

How the connection proves who's who

Every time the worker reaches out, it presents a certificate - a private credential belonging to it and 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, by hand: it's created for that one worker, and it's separately registered on that environment's own approved list.

Registered as approved added to this box's list Certificate created for this one worker Tourney web app opens only for a registered cert FOMX.ai Spark's worker reaches out first, every time shows its certificate No certificate, the wrong one, or one never registered - refused before the app ever runs.

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.

Other features, and what it's built with

Other features

  • Live Scores - in-progress and upcoming games appear on every standings page, refreshing every 30 seconds.
  • Upsets Tracker - automatically detects and lists every upset (higher seed beats lower seed), with affected bracket count.
  • Compare - pick any two entries head-to-head to see shared picks, unique picks, and current score difference.
  • Prediction Page - filter and sort entries by projected final score.
  • Tiebreaker - used only if two entries are tied; closest guess for championship total (without going over) wins.
  • Backup & Restore - admin can download a full JSON snapshot of all users, entries, and picks, and fully restore from it if needed.

Built with

App & hosting

FlaskFlask-WTF (CSRF)Flask-Limiter GunicornMySQLSQLAlchemy DockerAmazon EC2Nginx

Frontend

Bootstrap 5HTMXMermaid.js

Data sources

ESPN public APIsBeautifulSoupCollegeBasketballData.com

AI

Claude (Anthropic API)gpt-oss:20b via OllamaTailscale

Email & analytics

AWS SES / boto3DuckDBpandasscikit-learn (shelved pipeline)

Quality & CI/CD

ruffbanditpip-auditQcoder (AI review)GitHub Actions