Skip to content
Before the season

Build the fixture list once, then move what needs moving

Pick a single round robin, a double round robin with home and away swapped, or a games-per-team cap for a short season. The generator pairs every team, drops in a bye when the count is odd, and writes the fixtures and matches in one transaction — then tells you which of them clash with matches you already had. A whole tournament day is its own mode: one calendar block, a slot length you set, and a break inserted where lunch goes. Season round-robin generation is free. The Tournament Day Builder, pools and brackets are the paid Tournaments module.

A fixture list does not survive first contact with a venue

Building the grid is the easy part — an evening with a spreadsheet and a highlighter. What costs the season is everything after it. A team that plays in two divisions gets pencilled into both at the same time on a Tuesday night. The hall was already hired out and nobody told the fixtures secretary. The clocks go forward and the second half of the season sits an hour off, because every date was typed as local wall-clock time. Then it rains, four matches move, and the spreadsheet, the group chat and the noticeboard all disagree about kick-off.

How it works

Four steps, no configuration

  1. Put the venues in first

    Add the venues you actually use, with the address, the capacity and the notes the visiting side always asks for. Split each one into named courts or surfaces, so a fixture points at Court 2 rather than at a building.

  2. Choose the shape of the season

    Single round robin, double for home and away, or a cap so each team plays a set number of games rather than the full rotation. Those three shapes are free. For a one-day event there is the Tournament Day Builder instead — a start time, a slot length, and a break after the match you choose — and the Builder, along with pools and knockout brackets, is the paid Tournaments module.

  3. Generate, then read the warnings

    The whole plan is checked against the matches you already have before anything is written, and the overlaps come back as a list naming the clashing fixture. Generation does not stop for them — you get the list and decide. The slot length used for the check comes from the sport being played, not a flat hour. One thing to know up front: the venue check compares the venue, not the court inside it, so a multi-court hall reports overlaps you will wave through.

  4. Move what moves

    Rain, a broken heater, a cup replay. Reschedule, postpone or cancel any fixture from the schedule manager, or create a one-off match by hand with its own date, venue and court. If a double-booking is genuinely the plan, force it past the conflict check deliberately.

Capabilities

What the scheduler does

Everything below is built and running today. Unbadged items are included on every plan; a badge means the capability needs a module added to your plan, a higher plan tier, or a piece of setup finished first.

15 of these are included on every plan. One group needs the Tournaments module.

  • Automatic round-robin fixture generation

    Generate a full round-robin schedule for a league from its team list — the app pairs every team using the circle method, inserts a bye when the team count is odd, and writes the fixtures and matches in a single transaction.

    League-admin action only (LEAGUE_ADMIN / SUPER_ADMIN / PLATFORM_SUPPORT, or a club role holding the LEAGUE_MANAGE_SCHEDULE capability — backend/src/handlers/standingsHandler.ts:310). Requires an active season (backend/src/services/scheduleService.ts:227) and at least two non-deleted teams (backend/src/services/scheduleService.ts:214). An archived league is read-only, so a closed season's schedule cannot be regenerated.

  • Double round-robin (home and away)

    Choose a single or a double round-robin; the double pass replays every pairing with home and away swapped.

    The API accepts one or two passes only — a triple round-robin is rejected by the request schema.

  • Games-per-team cap (short-season / partial round-robin)

    Cap the schedule so each team plays at most a chosen number of games instead of the full round-robin.

    Implemented by truncating whole rounds, so with an odd team count the byes still fall where the rotation puts them.

  • Tournament-day scheduling (one-day block with slot spacing and a lunch break)

    Run a whole tournament day in one calendar block: every generated match gets its own start time spaced by a slot length you set, with an optional break inserted after a chosen match.

    Admin-only route (frontend/src/app/router/router.tsx:768). The round-robin/pool-play half is free; the knockout step of the Tournament Day Builder calls the paid, module-gated knockout generator. Slot spacing is a fixed interval (45 minutes unless you change it) — it does not read court rental hours or venue availability, so nothing checks that the day fits inside the hours you booked.

  • Team double-booking detection

    If you try to schedule or move a match into a window where one of the two teams is already playing, the app blocks the save and names the clashing fixture.

    It is a soft block: an authorized league admin can force the booking through by re-sending with an override flag (backend/src/handlers/matchHandler.ts:1059). Bulk generation never blocks — it returns the overlaps as warnings instead (backend/src/services/scheduleConflictService.ts:288).

  • Venue double-booking detection

    Scheduling a match into a window when the same venue is already hosting one raises a conflict that must be acknowledged before it saves.

    The server-side check compares the venue only — it does not consider which court within the venue, so two matches on different courts of the same venue are reported as a venue conflict. Court-level precision exists only as an advisory banner in the browser's schedule manager (frontend/src/pages/app/schedulerConflicts.ts:78), which never blocks a save.

  • Sport-aware match length for conflict windows

    Conflict detection sizes the slot from the sport being played — derived from the league's period or half length where the sport has a clock, and from the sport's own declared typical match length where it does not — plus a handover buffer, with a per-league slot-length override that wins over both.

    Match end times are not stored; the check assumes the proposed match and the existing one occupy the same length block.

  • Deliberate double-booking override

    Leagues that intentionally stack fixtures can force a booking through the conflict check, so the detector never traps an operator who knows what they are doing.

    Available to any caller already authorized to write the schedule; there is no separate approval step or audit-specific role for the override.

  • Bulk-generation conflict summary

    When you generate a batch of fixtures, the app checks the whole plan against existing matches before writing anything and reports the overlaps it found.

    Warning only — generation never blocks. It compares against pre-existing matches; overlaps inside the batch itself are not reported, because whole rounds are deliberately scheduled at the same time for multi-court venues. Venue conflicts are not part of this check either — the generators never assign a venue.

  • Manual / custom fixture creation, reschedule, postpone and cancel

    Create one-off matches by hand with a date, time, venue and court number, then move, postpone or cancel any match from the schedule manager.

    Admin surface — the schedule manager route is restricted to SUPER_ADMIN, PLATFORM_SUPPORT and LEAGUE_ADMIN (frontend/src/app/router/router.tsx:812); a custom club role holding LEAGUE_MANAGE_SCHEDULE can move matches via the API. Cancel is a status change, not a delete — the row stays on the schedule and can be moved back to SCHEDULED or POSTPONED (backend/src/handlers/matchHandler.ts:1302). Deleting it is a separate action.

  • Venue management

    Keep a venue list per league with a structured address, capacity, amenities and notes, and attach a venue to any match.

    Venue details are operator-facing and require tenant read access to list (backend/src/handlers/venuesHandler.ts:238).

  • Named courts / playing surfaces per venue

    Split a venue into named courts or surfaces, and add, rename, delete or reorder them as the building changes.

    Court names are a venue-configuration surface; assigning a court to a match is a manual choice in the match dialog, not an automatic allocation.

  • Daylight-saving-safe date arithmetic

    Rounds are spaced by exact intervals in UTC, so a season that crosses a daylight-saving change does not slide its start times.

    The trade-off is explicit: fixtures keep the exact interval rather than tracking local wall-clock time across a DST change.

  • Pool / division scheduling (a round-robin per pool)

    Tournaments module

    Generate a separate round-robin inside each pool, so a group stage schedules pool by pool.

    Creating a pool is gated on the paid TOURNAMENT_POOLS module (backend/src/handlers/divisionHandler.ts:237), which is a PRO-tier, per-league opt-in that ships disabled by default (backend/prisma/seed.ts:95). Divisions that are not pools are ungated.

  • Public fan-facing schedule

    Every public league gets a fan page with a Schedule tab showing its fixtures, readable without an account.

    Requires the league to be public and active or completed (backend/src/handlers/standingsHandler.ts:42); signed-in followers of an unlisted league can also read it. Draft leagues stay private, and the tab hides when the Scheduling module is switched off for that league (frontend/src/pages/fan/LeagueFanPage.tsx:69).

  • Calendar subscription feeds (iCal)

    Subscribe to a league, team, venue or single match as a calendar feed and open it in Apple Calendar, Outlook or Thunderbird.

    A private league's feed inherits the league's access gate. Each league feed returns a sliding window of matches around today, and is capped at a maximum number of events per feed (backend/src/handlers/calendarFeedHandler.ts:283).

Questions

Before you ask

Will it work out the venues and courts for us?

No. The generator sets the dates and the spacing between rounds; assigning a venue and a court to a match is a manual choice in the match dialog. There is no automatic court allocation, and binding a pool to a court stores that association without steering the pool's fixtures onto it. If court assignment matters to you, plan on doing it by hand.

Our multi-court venue runs four matches at once. Will it fight us?

It will warn you every time, and you carry on. This is the limitation to know before you plan a busy hall. The conflict check that runs on the server compares the venue, not the court inside it — so Court 1 and Court 3 of the same sports centre read as the same place, and four matches on four courts read as four matches in one room. Nothing is blocked: the warnings are advisory and the fixtures still save. The schedule manager in the browser does show a court-level advisory banner, but the server-side check stops at the building. On a multi-court site, budget for waving warnings through.

What if we need two fixtures to clash on purpose?

Force it through. The double-booking check is a soft block: an admin already authorised to write the schedule re-sends the save with an override and the fixture lands. Worth knowing before you rely on it — there is no separate approval step or override-specific role, so anyone who can edit the schedule can override it.

Our season crosses the clock change. Does that shift everything?

Rounds are spaced by exact intervals computed in UTC, so a daylight-saving change does not slide the start times of a generated season. The trade-off is stated rather than hidden: fixtures hold the exact interval rather than tracking local wall-clock time across the change. If a particular date needs to follow the local clock instead, move that fixture by hand.

Is the Tournament Day Builder included, or is it a module?

It is a module. The Tournament Day Builder, pool creation, bracket generation and the public bracket page are all the paid Tournaments module — the one-day calendar block, the slot spacing and the lunch break arrive with it, not before it. What is free is season fixture generation: single round robin, double round robin and the games-per-team cap, plus the conflict checks and the manual reschedule tools. A one-day event that is a straight round robin between every entrant is covered by the free generator. The moment you want pools feeding a knockout, that is Tournaments. Brackets take 4, 8, 16, 32 or 64 entrants exactly, with no automatic byes, so you pad the field to the next size yourself.

Once we generate, where do the fixtures show up?

On the league's public page, under a Schedule tab a visitor reads with no account — provided the league is marked public and is active or completed. Draft leagues stay private. There is also a calendar feed for a league, a team, a venue or a single match that people subscribe to in Apple Calendar, Outlook or Thunderbird; a league feed returns a sliding window of matches around today rather than the whole season at once.

Generate your first fixture list

Public leagues need no account to open.