Most club websites stop at the brochure stage. They tell people where to go, maybe show a few photos, and then hand the real work off to spreadsheets, chat groups, and a lot of manual coordination. Badminton Blazers goes further. It combines public-facing club pages with the operational tools a badminton community actually needs: player accounts, tournament registration, doubles scheduling, standings, rankings, and mobile-friendly updates.

This post walks through how the project is built, why the architecture looks the way it does, and what worked well while putting it together. I am keeping this write-up intentionally safe for sharing, which means I will reference configuration by environment variable name only and avoid exposing any real secret values, credentials, private keys, or environment-specific deployment details.

The product we were really building

Before talking about models, views, and containers, it is worth naming the real problem. A club like this has two very different audiences:

  1. People visiting the site for the first time, who need a clear public presence.
  2. Organizers and returning players, who need workflows.

That split drives the whole architecture.

The public side of the app lives in the website app. It handles home, about, gallery, rankings, results, and upcoming events. It is the polished front door.

The operational side is split into focused Django apps:

  • accounts manages phone-number-based authentication and profile data.
  • tournaments handles structured events, teams, brackets, and match results.
  • regulargames handles recurring play sessions, player invites, doubles pairings, and session scheduling.
  • dashboard provides organizer and player dashboards.

That separation matters. It keeps the codebase from collapsing into one giant "club app" full of tangled business rules. Each app owns a clear slice of the domain while still participating in one coherent user experience.

Why Django was the right fit

This project is built on Django 5.2, and that choice makes a lot of sense for the problem.

The club platform is not a single-page app pretending to be a product demo. It is a database-backed operations system with forms, permissions, admin workflows, dashboards, and lots of structured relational data. Django gives that to us without forcing us to assemble the basics from scratch.

A few framework-level decisions are especially valuable here:

  • Django's ORM is a strong fit for clubs, tournaments, sessions, teams, matches, invites, and profiles because these are naturally relational concepts.
  • Class-based generic views help keep CRUD-style flows readable without hiding too much behavior.
  • Django auth remains useful even with a custom user model.
  • The admin provides a practical operational backstop.
  • Templates are enough for the UI because the app is workflow-heavy, not animation-heavy.
  • Middleware, context processors, and static file handling cover a lot of the boring but necessary plumbing.

In other words, Django lets the project spend its complexity budget on badminton logic instead of framework glue.

The project layout

At the top level, the repository is organized around a standard but well-structured Django layout:

  • badmintonblazers/ contains settings, URL routing, and WSGI/ASGI entrypoints.
  • accounts/ contains the custom user model, forms, views, and account-related tests.
  • tournaments/ contains event, team, bracket, match, and rating logic.
  • regulargames/ contains weekly session workflows, invites, pairings, and push subscription logic.
  • website/ contains public pages, ranking pages, and site-wide metadata.
  • dashboard/ contains organizer and player dashboards.
  • templates/ and static/ contain the presentation layer.
  • deploy/ contains the container entrypoint and nginx configuration.

What I like about this layout is that it matches the mental model of the product. If I need to explain where a feature lives, I can usually do it in one sentence. That is a sign of healthy boundaries.

Custom authentication built around phone numbers

One of the first interesting decisions in the project is the account model. Instead of using usernames, the app defines a custom User model in accounts/models.py and makes phone_number the USERNAME_FIELD.

That shift is small technically, but big from a product perspective. For a sports club, phone numbers are often more natural than usernames or sometimes even email addresses. Organizers already communicate with players over phone-based channels, so the login identifier should reflect that reality.

The custom user model adds club-specific fields like:

  • display_name
  • phone_number
  • emergency_contact
  • is_event_coordinator
  • phone_verified
  • verification metadata for code-based phone checks

There is also a PlayerProfile model hanging off the user as a one-to-one relation. That profile stores club-specific details such as skill level, preferred event type, experience, and club-maintained rating points.

Two implementation details stand out here:

First, the project auto-creates PlayerProfile rows using a post-save signal. That is a simple move, but it removes an entire class of "profile missing" bugs from the rest of the app.

Second, the code keeps profile-facing names in sync across related models. When a user changes their name, linked player records used in competition features can be updated as well. That keeps public rankings and internal identity data from drifting apart.

Account verification without overengineering the first version

The account flow includes registration, login, logout, profile updates, password management, and phone verification.

The verification process is implemented as a pragmatic staged system:

  • a six-digit code is generated
  • a cooldown is enforced
  • codes expire after a fixed time window
  • successful verification clears the pending code and marks the phone as verified

Right now, delivery is stubbed through a helper that logs the code rather than sending it through a real provider. That is exactly the kind of tradeoff I like early in a project: build the workflow first, prove the state transitions, and leave the external integration as a clearly marked boundary.

The important part is that the code already models the real verification lifecycle. Swapping the stub for an SMS or WhatsApp provider later becomes an integration task, not a product redesign.

Modeling people twice: users and players

A subtle but useful design choice in the platform is that accounts.User and tournaments.Player are separate models.

That might seem redundant at first, but it solves a real problem. Not every player in a tournament or weekly game needs to be a full authenticated user right away. Organizers may need to add guest players, import participants, or invite people who have not registered yet.

The Player model is focused on competition and participation. It includes:

  • player names
  • optional email and phone
  • rating values
  • rating uncertainty
  • games played
  • inactivity tracking
  • invite tier information

This lets the competition system stay flexible. A person can exist in the sporting domain before they fully exist in the authenticated user domain.

When needed, the ensure_user_account method can bridge that gap by linking a player to a user account and ensuring downstream profile objects exist. That is a strong example of modeling for real-world club operations rather than idealized sign-up flows.

Tournament architecture: more than just CRUD

The tournaments app is where the project starts to get genuinely interesting.

The Tournament model supports multiple formats:

  • round robin
  • single elimination
  • groups plus knockout

That means the app is not just storing tournaments. It is encoding tournament logic.

The model also stores the operational details needed to turn a tournament into something playable:

  • start and end dates
  • location
  • registration status
  • team limits
  • number of available courts
  • match duration
  • registration deadlines
  • points rules
  • start time

This is what turns a generic event page into an event engine.

Teams as first-class objects

Instead of treating a doubles team as an ad hoc pair attached to a match, the project models Team explicitly. That makes standings, scheduling, registration, seeding, and bracket propagation much cleaner.

The Team model enforces important constraints:

  • a team must contain two distinct players
  • a named team must be unique within a tournament
  • a player cannot belong to multiple teams in the same tournament

Those rules are validated at both the model and database-constraint level. That is a good habit in workflow-heavy applications. If a rule matters to the business, do not leave it only in the form layer.

Match generation and bracket progression

The Match model carries more than scores. It stores:

  • round number
  • bracket position
  • court assignment
  • scheduled start time
  • competing teams
  • placeholder labels for future bracket slots
  • next-match linkage for knockout propagation

That allows the tournament system to support both known participants and future placeholders like seeded winners or qualifier slots.

Once a result is recorded, the app does several things:

  1. It validates that the match is not a draw where draws are invalid.
  2. It stores the result and marks the match complete.
  3. It applies player rating updates.
  4. It propagates the winner into the next match where relevant.
  5. It advances qualifiers when the tournament format requires it.

That chain is important. A score entry is not just data entry; it is a state transition that updates the competitive system.

Standings and tie-breakers

Tournament standings are not computed as a static denormalized table. They are derived from completed matches. The code tracks wins, losses, points for, points against, and points totals, then uses tie-breaker logic including points ratio and head-to-head comparisons where appropriate.

That keeps the leaderboard logic explicit and testable. It also means the standings can be recalculated consistently when past results change.

Regular games: the operational heart of the club

If tournaments are the flashy part of the system, regulargames is arguably the most club-realistic part.

Weekly or recurring sessions are where badminton communities actually spend most of their time, and this app is designed around that reality.

The RegularGameSession model stores:

  • session date
  • time
  • location
  • status
  • optional capacity
  • session format
  • number of courts
  • match duration
  • whether players are allowed to submit scores

This gives organizers a repeatable way to create play nights without inventing the workflow from scratch each time.

Bulk creation for recurring operations

One of my favorite details is the bulk session creation flow. Organizers can create many future sessions in one pass, and the code skips dates that already exist or fail validation.

That is exactly the kind of feature that says, "someone thought about what running a club actually feels like." It reduces repetitive admin work and acknowledges that regular play is usually planned in batches, not one event at a time.

Preferred partners and automatic doubles teams

The schedule builder in RegularGameSession.build_schedule is a particularly nice piece of domain logic.

It does not just randomize everyone blindly. It first tries to honor preferred partner selections using normalized phone numbers. Then it shuffles the remaining participants and forms teams from the leftovers.

That gives the app a more human feel. Club play is social, and preferred pairings matter. But the system still has a fallback when not everyone arrives with a fixed partner.

Round-robin generation for weekly sessions

Once teams exist, the session can generate matches using a round-robin algorithm. If the format is double round robin, it generates a second cycle with swapped order. Then it assigns courts and start times based on the number of available courts and the configured match duration.

That sequence is powerful because it converts a list of confirmed players into an actual playable evening:

  • participants become teams
  • teams become fixtures
  • fixtures become scheduled court slots

This is exactly where software saves organizers from whiteboards and last-minute improvisation.

Invite tiers and acceptance flows

The regular games system also includes a smart invitation layer.

RegularGameInvite supports:

  • direct player invites
  • invites by name, email, or phone
  • invitation tiers
  • expiration windows
  • acceptance tracking
  • conversion from invited person to participant

Tiering is a thoughtful addition. It allows organizers to stagger invitations, which is useful when sessions fill up quickly or when a club wants to prioritize core players first and widen the pool later.

The acceptance flow also resolves or creates the necessary player record. That means the invite system is not just messaging. It is part of the participant pipeline.

Ratings and rankings: lightweight, useful, and explainable

The platform includes a rating system that is more nuanced than a simple win counter but still practical enough for club use.

The tournaments/ratings.py module defines a configurable rating model with:

  • a baseline rating
  • uncertainty (sigma)
  • a floor and cap for uncertainty
  • inactivity growth
  • margin-sensitive updates
  • team expected-score calculations

This is a nice middle ground between something trivial and something academically overcomplicated. The system acknowledges that not all wins are equally informative and that inactive players should become less certain over time.

One especially good design choice is the conservative leaderboard rating. Instead of ranking players by raw estimated skill alone, the app uses a conservative score based on rating minus uncertainty. That discourages volatile rankings for players with little match history and makes the leaderboard feel more trustworthy.

The public rankings page then rebuilds standings from completed matches across both tournaments and regular game sessions. That is an important product decision. It tells players that the platform sees their overall club participation, not just isolated event records.

Dashboards that turn data into coaching and operations

The dashboard app adds a second layer on top of raw competition records.

There is an organizer-oriented dashboard that surfaces counts and upcoming activity, but the more interesting part is the player dashboard. It aims to show a player more than just "you won" or "you lost." It starts to answer questions like:

  • How am I trending?
  • Who do I play well with?
  • How active have I been?
  • Which pairs are performing well lately?

The code aggregates completed matches across tournaments and regular sessions, computes pair-level stats, caches heavier calculations, and builds a more personalized experience out of the raw event history.

That is a strong example of layering product value on top of existing data. Once the operational system is solid, analytics become much easier to add.

The frontend approach: server-rendered, fast, and appropriate

This project does not try to impress by turning every screen into a JavaScript application. That restraint is a strength.

The frontend is built with Django templates in templates/ and shared assets in static/. Bootstrap-based styling keeps forms and layouts consistent, while custom CSS and branded assets provide the club identity.

Server-rendered HTML is a good fit here because:

  • forms and validation matter more than client-side state choreography
  • most pages are read-heavy and workflow-driven
  • the SEO-facing public pages benefit from regular HTML responses
  • the maintenance cost stays lower for a club-scale product

There are also signs of thoughtful UI ergonomics throughout the codebase:

  • paginated session lists
  • grouped match displays by round
  • permission-aware controls
  • organizer-only workflows
  • player-friendly registration and score submission paths

This is what practical frontend work looks like in an operations product: clarity over spectacle.

PWA support and push notifications

Even though the frontend is mostly server-rendered, the app still picks up modern mobile-friendly capabilities.

The root URL configuration serves both a web app manifest and a service worker. The manifest defines installable app metadata, shortcuts, icons, and screenshots. The service worker caches core static assets, supports offline-friendly static fetches, and handles push notifications.

On the client side:

  • static/js/pwa.js ensures a service worker is registered at the root scope
  • static/js/push.js manages push enablement, permission requests, subscription state, and unsubscribe behavior

On the backend side:

  • VAPID keys are loaded from environment variables
  • push subscriptions are stored in WebPushSubscription
  • pywebpush is used to send notifications
  • invalid subscriptions can be marked inactive when providers reject them

This is a strong example of progressive enhancement. The app still works as a normal website, but on supported devices it can behave more like a club companion app.

Site-wide metadata, SEO, and discoverability

The project does not ignore the boring-but-important public-web details.

There is a context processor that injects site metadata and current club context into templates. There are sitemap definitions wired into /sitemap.xml. There is a robots.txt route. Rankings and results pages have caching applied where it makes sense.

That means the app is not just trying to be useful after login. It also cares about being discoverable, indexable, and presentable as a public club presence.

For many real organizations, that matters just as much as the admin workflows.

Local development: simple by default

One of the nice things about the setup is that local development starts simple.

The development settings use SQLite by default, .env loading is built in through python-dotenv, and the project can be booted with familiar Django commands:

  • create a virtual environment
  • install dependencies
  • provide a .env with required variable names
  • run migrations
  • create a superuser
  • run the development server

That is the right default. You do not need to bring up Postgres and nginx just to work on a form or tweak a leaderboard template.

Keeping the local path lightweight lowers the friction for maintenance and future contributors.

Production and deployment: Docker, Postgres, Gunicorn, nginx

When it is time to deploy, the architecture grows up appropriately.

The repository includes:

  • a Dockerfile based on python:3.13-slim
  • a docker-compose.yml with db, web, and nginx services
  • a production settings module that switches the database to Postgres
  • an entrypoint script that runs migrations and collects static files before starting Gunicorn

This is a practical deployment shape for a Django application:

  • Postgres handles relational data reliably in production.
  • Gunicorn serves the Django app process.
  • WhiteNoise handles compressed manifest static files inside the Django layer.
  • nginx sits in front to serve traffic and static/media files cleanly.

I also like that the entrypoint takes responsibility for migrate and collectstatic. It means container startup aligns closely with the operational needs of the app.

Security and safety considerations

Any time you write publicly about a real project, you need to be careful not to turn the blog post into a reconnaissance document. A few boundaries matter here.

First, secrets belong in environment variables, not in prose. In this project that includes:

  • SECRET_KEY
  • database credentials
  • push notification VAPID keys
  • production host configuration

Second, development shortcuts should be treated as development shortcuts. The phone-verification sender is still a stub, and auto-provisioned account flows should evolve into a stronger invitation or password-setup process before broader production use.

Third, production hardening is partly present and partly still an operational responsibility. The project already supports DEBUG=False, host restrictions, WhiteNoise, and Postgres-backed production settings. But as with any real deployment, monitoring, secret rotation, provider integrations, HTTPS termination, and access control hygiene still matter.

The good news is that the codebase already has the right boundaries to improve these areas without rewriting the app.

Testing and confidence

The repository includes targeted tests around several of the riskier parts of the system, including:

  • tournament match generation
  • groups-and-knockout behavior
  • player, team, and tournament management permissions
  • duplicate-player constraints
  • standings tie-breakers in both tournaments and regular games

That test selection makes sense. The highest-risk bugs in a system like this are usually not "the page did not render." They are business-rule bugs: duplicate entries, broken draws, invalid standings, and permission mistakes.

If I were extending the suite further, I would add more end-to-end coverage around registration, invite acceptance, score submission, and the rating pipeline. But the existing tests already focus on the domain rules that would be painful to debug manually.

What I would call the strongest architectural decisions

Looking back at the project, a few choices stand out as especially strong:

  • separating authenticated users from competition players
  • giving recurring games their own app instead of forcing everything through tournaments
  • modeling teams and matches as real domain entities
  • treating ratings as a first-class system rather than a cosmetic number
  • using server rendering for most workflows while still layering in PWA features
  • supporting both simple local development and a more production-like Docker deployment

These are the kinds of decisions that keep a club platform usable after the first demo.

What I would improve next

No real project is ever "done," and this one already points toward some natural next steps:

  • replace the verification stub with a real messaging provider
  • harden account bootstrap flows so invited players complete secure password setup
  • add more integration tests for account, invite, and score-submission flows
  • improve observability with structured logging and deployment health checks
  • consider asynchronous job handling if push delivery or messaging volume grows
  • expand player analytics and season-level history views

None of those require changing the app's core shape. They build on the current foundation, which is a good sign.

Final thoughts

What I like most about Badminton Blazers is that it feels like software written for an actual community, not just a tutorial exercise dressed up with sports branding.

The codebase understands that badminton clubs run on repeated coordination: signups, pairings, scores, rankings, invites, schedules, and public trust. Django provides the backbone, but the real value comes from the domain decisions layered on top of it.

That is the difference between building a website and building a working club platform.

And that, to me, is what makes this project worth writing about.

About Me

Abhyuday Singh

I build products that mix software, data, automation, UI/UX, and AI, and I like writing about what I learn along the way.

Get in touch