{"id":73,"date":"2026-02-18T19:52:46","date_gmt":"2026-02-19T01:52:46","guid":{"rendered":"https:\/\/www.dirtymindsinc.net\/?p=73"},"modified":"2026-02-18T19:52:46","modified_gmt":"2026-02-19T01:52:46","slug":"football-squares-a-self-hosted-family-game-platform","status":"publish","type":"post","link":"https:\/\/www.dirtymindsinc.net\/?p=73","title":{"rendered":"Football Squares: A Self-Hosted Family Game Platform"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Whether it&#8217;s the Super Bowl or a regular Sunday game, Football Squares is a classic way to make watching NFL games more fun. I built <strong>Football Squares<\/strong> \u2014 a full-stack web application that handles everything from player registration to winner payouts \u2014 to run these games for friends and family without relying on third-party platforms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a look at what it does, how to run it, and where to find the source.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">What Is Football Squares?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Football Squares is a 10\u00d710 grid game. Each square on the board corresponds to a combination of the last digit of each team&#8217;s score. Players claim squares before the game, random numbers are assigned to the rows and columns, and winners are determined at the end of each quarter based on the scoreboard.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This application handles the full lifecycle:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Players register, browse active games, and claim squares from a live board<\/li>\n\n\n\n<li>Admins create games, pull in real NFL matchups from the ESPN API, assign random numbers, record scores, and track payments<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Features<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">For Players<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Registration &amp; Login<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Secure account creation with password strength requirements (12+ characters, mixed case, digits)<\/li>\n\n\n\n<li>Session-based authentication with HTTP-only cookies<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Game Browser<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Browse all active games at a glance<\/li>\n\n\n\n<li>Each game card shows the matchup, price per square, and current status (open, locked, or completed)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Interactive Game Board<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A live 10\u00d710 grid that auto-refreshes every 5 seconds<\/li>\n\n\n\n<li>Color-coded squares:<\/li>\n\n\n\n<li><strong>Green<\/strong> \u2014 available<\/li>\n\n\n\n<li><strong>Blue<\/strong> \u2014 yours<\/li>\n\n\n\n<li><strong>Gray<\/strong> \u2014 claimed by another player<\/li>\n\n\n\n<li><strong>Gold border<\/strong> \u2014 paid<\/li>\n\n\n\n<li><strong>Gold badge<\/strong> \u2014 a winner square<\/li>\n\n\n\n<li>Random numbers (0\u20139) appear along the edges once assigned by the admin<\/li>\n\n\n\n<li>Set a custom display name for your squares (shown on the board instead of your username)<\/li>\n\n\n\n<li>Printable board layout for game-day display<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>My Squares Dashboard<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>See all your squares across every game<\/li>\n\n\n\n<li>Breakdown of how much you owe and how much you&#8217;ve paid<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">For Admins<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Game Management<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Create games manually or load upcoming NFL matchups from the ESPN API (with a 5-minute cache)<\/li>\n\n\n\n<li>Set team names, game date, and price per square<\/li>\n\n\n\n<li>Assign random numbers to the board (one-time, irreversible \u2014 keeps it fair)<\/li>\n\n\n\n<li>Change game status: open \u2192 locked \u2192 completed<\/li>\n\n\n\n<li>Delete games<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Score Entry &amp; Payouts<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Enter scores quarter by quarter (Q1\u2013Q4) plus final<\/li>\n\n\n\n<li>The application calculates the winning square for each period automatically<\/li>\n\n\n\n<li>Payout amounts display at 20% of the full board value per quarter<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Payment Tracking<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Per-game payment summary: who owes what, how much has been collected<\/li>\n\n\n\n<li>Mark individual squares or all squares for a user as paid in one click<\/li>\n\n\n\n<li>Full cross-game payment overview per user<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>User Management<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>View, edit, and delete user accounts<\/li>\n\n\n\n<li>Promote or demote admin privileges<\/li>\n\n\n\n<li>Reset passwords on behalf of users<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Security Highlights<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Passwords hashed with bcrypt<\/li>\n\n\n\n<li>Rate limiting on login (10\/min), registration (5\/min), and square selection (100\/30 min)<\/li>\n\n\n\n<li><code>SELECT FOR UPDATE<\/code> database locking prevents race conditions on square selection<\/li>\n\n\n\n<li>XSS-safe custom display names (HTML-escaped, 50-character limit)<\/li>\n\n\n\n<li>Audit logging for logins, square changes, payment updates, and admin actions<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Running It<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Quick Start with Docker Compose<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The easiest way to run Football Squares is with Docker Compose. Pull the latest production images and start the stack:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Create an env file\ncat &gt; .env &lt;&lt;EOF\nSECRET_KEY=$(openssl rand -hex 32)\nCORS_ORIGINS=http:\/\/localhost\nEOF\n\n# Grab the production compose file\ncurl -O https:\/\/git.dirtymindsinc.net\/jwerle\/football-squares\/-\/raw\/main\/docker-compose.production.yml\n\n# Start it up\ndocker compose -f docker-compose.production.yml up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The frontend will be available on port <strong>80<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Create an Admin Account<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose -f docker-compose.production.yml exec backend \\\n  python create_admin.py &lt;username&gt; &lt;email&gt; &lt;password&gt;<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Container Images<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Production images are published to Harbor on every tagged release and are signed with cosign for supply-chain integrity.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Service<\/th><th>Image<\/th><\/tr><\/thead><tbody><tr><td>Backend<\/td><td><code>harbor.dirtymindsinc.net\/public\/football\/backend:latest<\/code><\/td><\/tr><tr><td>Frontend<\/td><td><code>harbor.dirtymindsinc.net\/public\/football\/frontend:latest<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pull them directly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker pull harbor.dirtymindsinc.net\/public\/football\/backend:latest\ndocker pull harbor.dirtymindsinc.net\/public\/football\/frontend:latest<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify a signature with cosign:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cosign verify \\\n  --key &lt;cosign-public-key&gt; \\\n  --insecure-ignore-tlog \\\n  harbor.dirtymindsinc.net\/public\/football\/backend:latest<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Tech Stack<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Layer<\/th><th>Technology<\/th><\/tr><\/thead><tbody><tr><td>Frontend<\/td><td>React, React Router, Axios<\/td><\/tr><tr><td>Backend<\/td><td>Python, Flask, SQLAlchemy<\/td><\/tr><tr><td>Database<\/td><td>SQLite (persisted via Docker volume)<\/td><\/tr><tr><td>Auth<\/td><td>Session cookies, bcrypt<\/td><\/tr><tr><td>CI\/CD<\/td><td>GitLab CI, kaniko, cosign<\/td><\/tr><tr><td>Registry<\/td><td>Harbor<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<h2 class=\"wp-block-heading\">Source Code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The full source is available on GitLab:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/git.dirtymindsinc.net\/jwerle\/football-squares\">git.dirtymindsinc.net\/jwerle\/football-squares<\/a><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Found a bug or have a feature request? Open an issue:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/git.dirtymindsinc.net\/jwerle\/football-squares\/-\/issues\/new\">Submit an Issue<\/a><\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<p class=\"wp-block-paragraph\">Contributions, feedback, and forks are welcome. Whether you&#8217;re hosting a Super Bowl party or just making Sundays a little more interesting, Football Squares is ready to run.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Whether it&#8217;s the Super Bowl or a regular Sunday game, Football Squares is a classic way to make watching NFL games more fun. I built Football Squares \u2014 a full-stack web application that handles everything from player registration to winner payouts \u2014 to run these games for friends and family without relying on third-party platforms&#8230;.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-73","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=\/wp\/v2\/posts\/73","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=73"}],"version-history":[{"count":1,"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=\/wp\/v2\/posts\/73\/revisions"}],"predecessor-version":[{"id":74,"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=\/wp\/v2\/posts\/73\/revisions\/74"}],"wp:attachment":[{"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=73"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=73"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dirtymindsinc.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=73"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}