Matchboard is a private coach-facing youth football operations cockpit for match-round squad planning, controlled player movement, coaching intent, matchday responsibility, plan integrity signals/explainability, finalized history, and post-match reflection across a league season.
It is deployed as a hosted web app on Vercel with Neon PostgreSQL backend persistence. It is not a generic club-management platform, not a parent communication platform, and not a public player evaluation system.
Selections are generated per match round. Fairness is evaluated across the season/league season.
The app plans squads for already-created matches. It does not auto-create fixtures, schedule a season, or manage a club.
The primary workflow is:
- Setup — Add teams, add players, add matches. Mark player availability.
- Define intent — Set match purpose, team risk, desired football behavior, support need, development focus.
- Populate all — Generate draft selections for all rounds in the active league season. Each round uses round-level orchestration (not match-by-match). No round is finalized.
- Review — Inspect draft selections, plan integrity signals, fairness impact, explanations, and coaching intent alignment. Resolve blockers. Manually adjust draft squads if needed.
- Adjust — Manual changes are allowed. Manual changes must show impact. Manual changes must preserve auditability.
- Finalize — Lock one round at a time, or lock individual matches within a round. Finalized rounds and matches become history and cannot be silently mutated.
- Reflect — Record team-level reflection. Record player-level feedback only where useful. Use observable behavior.
- Learn — Use history, readiness, feedback, and fairness to inform later planning. Do not mutate finalized historical plans.
The central operating flow is: Assistant → Fixtures → Round Board → Match reporting → Season/History review.
The canonical primary navigation is: Assistant, Fixtures, Teams, Players.
- Assistant (
/assistant) shows the next action based on workflow state. Derives work items from live database state, not from persisted issue rows. - Fixtures (
/fixtures) provides the league-season and round hierarchy. Completed match results are shown directly in fixture rows. - Round Board is the primary squad decision surface.
- Teams provides selected-league-season team results and team detail workspaces. Team rules remain in team detail.
- Players (
/players) provides three modes: Season overview, Current round attention, and Manage base groups. - Season, History and Rules are secondary analysis/configuration destinations.
The Players page is the coach-facing overview for participation statistics, current planning attention, and base-group administration.
- Season overview (default): factual player matrix with actual participation and recorded match statistics for a selected league season. No summary panel or Movement paths overview. Source of truth is reported or locked post-match data. Draft selections and finalised unreported assignments do not count as played.
- Current round attention: canonical live plan-integrity state for a selected round. Uses
computeRoundPlanIntegrityonly. Does not derive attention from goals, assists, or historical movement. - Manage base groups: stable core-team assignment and player registry administration. Separate from weekly match selection and seasonal review.
Played, Goals, Assists, Core, Support, Development, Matchday additions, Planned absent. All scoped to the selected league season. Goals and assists are factual statistics only — they do not affect selection generation or fairness.
Covered, Decision required (available eligible without planned match opportunity), Blocked (selected unavailable, invalid plan), Not available, Unconfirmed. Derived from canonical live plan integrity only.
- No overall fairness score, player ranking, or hidden automatic player judgement.
- Season overview does not render a summary panel, Movement paths section, or automatic fairness badges.
- No stat-driven squad selection.
- Matchday additions are factual context, not warnings.
- Actual additional appearances are factual load context, not current attention states.
- Base-group management is separate from match planning and seasonal review.
- Coach-facing only. Not included in parent-facing exports.
Season is the full football-year context. Each Season has a year field identifying the football year (e.g. 2026). LeagueSeason is the bounded spring/autumn operational window, with an auto-derived part field (SPRING or FALL) based on its date range.
User-facing text uses "League season" and "Season", never "Planning period" or "Phase". League season display labels are derived from startDate, endDate, and the part field. A spring league season spanning April to June 2026 shows as "Spring 2026 · Apr–Jun". An autumn league season spanning August to October 2026 shows as "Autumn 2026 · Aug–Oct". A misleading stored name like "April 2026" for a multi-month scope is never shown alone — the visible label must communicate the actual date range.
The Teams page shows selected-league-season completed match results. Required columns: Team, Played, W-D-L, GF, GA, GD, Clean sheets, Core players. Team rules, squad limits, support priority and rotation paths remain in team detail (/teams/[teamId]).
Team result statistics derive from completed post-match reports only (REPORTED or LOCKED). DRAFT reports do not count as results. Team GF/GA derives from PostMatchReport score values, not from player Goal events.
Fixtures show completed final score and W/D/L outcome directly in fixture rows/cards. A DRAFT post-match report is incomplete work and never a final result. Past matches with DRAFT reports show "Report incomplete" rather than a draft score.
Completed fixtures use subtle outcome styling: soft green for Won, soft neutral/slate for Drawn, soft red for Lost. Outcome text is always visible — colour is secondary reinforcement only. No outcome styling is applied to upcoming matches, DRAFT reports, or report-incomplete tasks.
Matches can be rescheduled before a completed post-match report exists. Edit date and kick-off time from Match details. Inside the current LeagueSeason, Matchboard automatically places the match in the correct weekly round. When the target week does not yet have a round, Matchboard creates it automatically. Draft planning follows the moved match and integrity is recalculated. A finalised squad plan must be unfinalised before moving the match to another round. Completed match history cannot be rescheduled through ordinary editing. Moving outside the current LeagueSeason requires separate league season handling.
A cancelled match is a scheduled fixture that did not happen. The coach can cancel a match from the match detail page by providing a reason. Matches with a completed post-match report cannot be cancelled. Cancelled matches are excluded from draft generation, plan integrity computation, finalization, post-match reporting, and season statistics. Planned selections for cancelled matches are preserved as context but do not count as played appearances. The coach can reopen a cancelled match to restore it to scheduled status, making it available for normal planning again.
"After match" opens the reporting workspace directly. When no report exists and a finalised squad exists, the first entry creates a DRAFT report seeded from the planned squad automatically — no separate "Open report" or "Seed from plan" step. Reports use a single "Complete report" action instead of separate Submit and Lock steps. Post-match feedback applies only to actual participants with confirmed PRESENT attendance, including manually added matchday participants.
The default Season overview is a compact factual matrix with columns: Player, Core team, Played, Goals, Assists, Core, Support, Development, Matchday additions, Planned absent. It does not show round-by-round columns, Last movement, Review, dropped status, or a separate Profile button. Player names link to full profiles.
The interface prioritises actionable football decisions over configuration exposure.
- RotationPath is the single source of truth for non-core player movement. A player may only be selected outside their core team when an active directed RotationPath exists from core team to target team for the exact role being assigned, unless manually overridden with a reason. Each path authorizes exactly one role: SUPPORT, DEVELOPMENT, or BACKFILL. A SUPPORT path does not permit DEVELOPMENT or BACKFILL movement. Paths are directional. No path means no automatic non-core selection. Fairness scoring cannot make an invalid path valid. The legacy
TeamSupportSourceandTeamDevelopmentSourcetables must not drive selection eligibility — they are scheduled for removal. - MovementCandidate marks individual players as preferred for temporary movement through a specific rotation path. It is a soft preference that augments RotationPath eligibility and existing player attributes. It does not bypass hard eligibility rules (RotationPath, nonRotatable, same-round conflict). Active candidates receive a +12 scoring bonus in the selection engine. Non-candidate eligible players can still be selected. Paused candidates are excluded from scoring but the record is preserved. SUPPORT candidates are allowed on BACKFILL rotation paths; DEVELOPMENT candidates are allowed on CONFIDENCE_REBUILD paths. Movement candidates are coach-facing only — never included in parent-facing exports or external AI payloads. Selecting a non-candidate player for non-core movement shows a contextual note but does not block the manual override.
- Team support is priority 1. Required support must be fulfilled before development movement, fairness optimization, cosmetic balancing, or generic rotation. If required support cannot be fulfilled, a Blocked or Decision required condition is generated — the team is never silently weakened. Support priority uses ascending sort order: lower number = higher priority (priority 1 is resolved before priority 2).
- Squad repair follows support movement. When a player fills a squad gap caused by support/development movement, the generation engine assigns
role = SUPPORTwith a squad repair explanation code.BACKFILLremains in the Prisma enum for backward compatibility of historical data and manual overrides. The user-facing term is "squad repair", not "backfill". Squad repair priority: (1) own core team player moved as support if matches are on different dates, (2) players from teams connected by an active DEVELOPMENT or SUPPORT rotation path to the receiving team wherenonRotatable = false, (3) any other player from another team with an active SUPPORT or BACKFILL rotation path wherenonRotatable = false. Non-rotatable players are never used as generic squad repair. - Movement ledger is mandatory. Every non-core player movement must create a MovementLedger entry. Support, development, and squad repair from another team all create ledger entries. The movement ledger is the authoritative record of player movement. The export must never show empty movements when non-core selections exist.
- Same-round player uniqueness is the default. A player must not be planned for two matches in the same round/week. The generation engine does NOT produce controlled double-load. Existing historical data with
controlledDoubleLoad = truemust still be readable. Actual double-load from post-match reports is tracked through effective participation/history and must not mutate finalized planned selections. Additional actual appearances from post-match reports are recorded separately as unplanned participation and do not mutate finalized planned selections. - Target squad size is a planning target, not a hard cap. A team may be selected above target up to
maxSquadSize. BelowtargetSquadSizebut aboveminAcceptedSquadSizegenerates a Planning note. BelowminAcceptedSquadSizeis a hard floor requiring manual override. - The match round is the operational planning unit. The season/league season is the fairness and load-balancing context.
- Plan integrity signals are persisted to the database and read back by the UI and finalization logic. Current active integrity is computed from the current editable draft — signals belonging to earlier drafts no longer appear after their cause is resolved. Resolving a condition removes it from current work. Blocked conditions prevent normal finalization (require conscious override with reason). Decision required conditions allow finalization with a recorded reason. Planning notes are informational only and do not block finalization or create active work items. Fixtures displays structured Blocked and Decision required summaries only — never generic issue totals. Round Board shows Plan integrity, Planning notes and Why this selection — never actionable warnings or generic warning counts. Selection rationale is explanation only and is never counted as unresolved work. Planned selections and actual participation remain separate.
- Draft selections are editable and not final history. The coach can manually add, remove, change role, or replace players in draft match squads. Manual edits use the same domain validation as automatic generation. UI-only validation is not enough.
- Finalized rounds become hard history. Finalized selections cannot be edited without an audit trail.
- Manual override requires reason. When a manual edit bypasses a hard rule, the reason must include a structured category (squad_too_small, support_missing, development_opportunity, no_planned_match_opportunity, double_load_needed [legacy only], availability_changed, coach_judgement, match_already_played, data_correction, other) and free-text detail. Generic "Manual override" alone is not sufficient. The reason must be persisted with the selection and appear in the finalization summary.
It is not a generic club-management platform, not a parent communication platform, and not a public player evaluation system.
Matchboard supports a coaching loop: intent → selection → responsibility → execution → reflection → learning.
Coaching intent can be attached to league seasons, match rounds, matches, teams, and selections. Intent categories include team_first, reset_after_error, support_teammates, positional_discipline, play_through_team, defensive_recovery, confidence_rebuild, challenge_exposure, stabilize_weaker_team, and protect_match_function.
Intent informs explanations and plan integrity signals but does not silently override hard eligibility rules. Intent can be edited by the coach before finalization. Intent remains coach-facing unless explicitly exported through neutral parent-safe language. Finalized history preserves intent snapshots from finalization time.
A selected player may receive a matchday responsibility, separate from the selection role. Responsibilities are coach-facing execution concepts: stabilizer, connector, recovery_leader, width_holder, challenge_player, confidence_rebuild_player.
Responsibilities must be coach-facing by default, preserved in finalized history, never change player eligibility, explained using observable football language, separate from player identity or permanent labels, and may change from match to match.
Readiness is soft coaching context, not a hard ranking system. Initial readiness signals: effort trend, attendance reliability, learning behavior, team-first behavior, reset-after-error reliability, coach trust.
Low readiness cannot automatically exclude an eligible player. Strong readiness cannot automatically override hard eligibility rules. Readiness must be coach-editable, time-bound, based on observable behavior, and excluded from parent-facing exports. Readiness must not create automatic punishment, permanent labels, or parent-visible judgement.
Matchboard supports lightweight post-match feedback based on observable behavior. Feedback categories: effort, team help, reset after mistake, positional discipline, teammate involvement.
Feedback is coach-facing by default, describes behavior not character, is optional and lightweight, and must not shame players or become automatic punishment. Disallowed feedback language includes: lazy, selfish, bad attitude, weak player, not good enough, useless, problem player. Allowed language uses observable behavior: helped teammate after ball loss, recovered position quickly, stayed available for pass.
Feedback can inform future plan integrity signals, readiness signals, and planning suggestions. Feedback must not mutate finalized planned selections. Actual participation belongs to post-match reality/history and must stay separate from planned selection.
Matchboard supports reusable opponent-team registration and match-linked encounter history. After a match, coaches can record private observations about the match environment (opponent players, coaching staff, spectator/sideline context) and sporting fit through existing match-fit feedback.
The feature is coach-facing. It records encounters rather than rating opponents. It does not store opponent individual identities. It does not automatically alter squad selection. It is not a public rating, blacklist, or formal incident-reporting system.
Opponent observations are separate from sporting-match-fit feedback. The existing Match.matchFit field is reused as the sporting-fit context for encounters. No duplicate sporting-fit model exists.
Post-match observations use neutral, observable language: "Fair Play concern", "Observed concern", "Serious concern observed". They never use labeling language like "Bad team", "Unsafe team", or "Risk team".
Match-environment observations, concern categories, factual summaries, and follow-up status are excluded from parent-facing exports and external AI payloads. Opponent-team display names may appear as normal fixture information in parent-facing contexts.
The selection engine is unchanged: opponent identity, match-fit history, and environment observations do not alter eligibility, support priority, development movement, squad repair, fairness, readiness, plan integrity signals, blockers, or finalisation.
Internal planning reasons, readiness notes, support burden, confidence rebuild, effort concerns, and execution feedback must not leak into parent-facing exports.
Coach export includes: roles, movement direction, explanations, override reasons, readiness notes, and feedback. Parent export uses neutral language: rotation, suitable challenge, team balance, availability, match experience, development opportunity, squad adjustment. Parent export must never include: low readiness, weak player, support burden, confidence rebuild, effort concern, coach trust, needs_attention, internal ranking, punishment, selection debt, culture debt, or hidden judgement.
Player names and personal data must not be sent to external AI services. Use stable player IDs and sanitize payloads. Hosted architecture does not make coach-facing data public.
Every non-obvious selection should be explainable through: selection role, movement path or manual override, coaching intent, matchday responsibility, relevant plan integrity signals (blocked conditions, decisions required, planning notes), fairness impact, load impact, support impact, risk created or mitigated, distinction between hard rule and scoring preference, and distinction between planned selection and actual participation.
Manual changes are allowed, but the app must explain impact. Manual changes should support real matchday reality: late absence, emergency support, sickness, injury, availability change, coach judgement, squad size repair, real-world backfill, and actual participation differing from planned selection.
Adding a player manually recalculates plan integrity signals, round status, match status, explanations, fairness impact, and movement ledger. Emergency backfill close to matchday is recorded as actual participation, not retroactively pretending the generation engine planned it. Actual double-load caused by real-world backfill is tracked through effective participation/history and must not mutate finalized planned selections. Manual removal preserves audit history. Manual changes require coach-facing explanation if they violate normal rules or create notable fairness/load/support impact.
Matchboard is a private coaching app. Coach-facing data — readiness signals, execution feedback, internal explanations, coaching intent, matchday responsibilities, support burden — must remain coach-facing by default.
Player names and personal data must not be sent to external AI services. External payloads must use stable player IDs and sanitize personally identifiable information. Hosted deployment on Vercel with Neon PostgreSQL does not weaken privacy boundaries. Coach-facing data remains private by default.
Matchboard must not become: a punishment engine, a hidden player ranking ladder, a moral scoring system, a parent-visible judgement tool, a tool for hard early sorting, a fake equality generator, a generic scheduling system, a generic club-management system, or a public player evaluation system.
Low readiness cannot automatically exclude a player. Feedback cannot be shown in parent export. Movement remains temporary and explainable. Stable belonging remains protected. Coach judgement remains explicit when overriding rules. Hosted deployment does not weaken privacy boundaries. Player development context does not become public labels. Stronger players can be used for support without permanently redefining their identity. Weaker but hungry players can receive challenge where behavior and context support it. Social participation is respected, but it must not silently define the football ceiling for the whole group.
| Status | Meaning |
|---|---|
| NOT_GENERATED | No selections yet |
| DRAFT | Selections generated, not finalized |
| BLOCKED | Draft with Blocked conditions |
| READY | Draft with no blockers |
| FINALIZED | Locked history |
BLOCKED and READY are derived status values — they are not stored in the database. BLOCKED = draft + Blocked condition. READY = draft + no Blocked or Decision required conditions.
Populate all generates draft selections for all non-finalized rounds in the active league season in one action. It groups matches by round and generates per round using round-level orchestration. It processes rounds in chronological order. Draft selections from earlier rounds may be used as provisional planning context for later rounds in the same run. Populate all does not finalize any round. On partial failure, successful round generations are kept and failures are reported.
After populate all, each round has draft selections, plan integrity signals, and explanations. The coach reviews plan integrity signals by round, fixes issues per match, and may manually adjust draft squads before finalization. Rounds with Blocked conditions require conscious override to finalize. Rounds with Decision required conditions can be finalized with a recorded reason.
Draft match squads can be manually edited before finalization. The coach can add players, remove players, change player roles, or replace players. Manual edits use the same domain validation as automatic generation — UI-only validation is not enough. Every non-core movement must have a valid RotationPath or an explicit manual override reason. Same-round conflicts, availability, squad size, and non-rotatable rules are all validated. Manual edits recalculate match status, round status, plan integrity signals, explanations, and fairness impact. Finalized selections cannot be edited by normal draft actions.
Draft selections can be cleared at three levels:
- Clear all — removes all non-finalized draft data (selections, plan integrity signals, explanations, movement ledger, provisional context) across all rounds in the league season
- Clear round — removes draft data for one selected round only
- Clear match — removes draft data for one selected match only
Clear actions preserve finalized selections, finalized history, teams, players, matches, rounds, rules, and availability. After clearing, affected rounds return to not-populated state and round status is recalculated. Clear all requires explicit confirmation before executing.
Finalizing a round locks all selections as immutable history. The app checks for Blocked conditions before allowing finalization. Finalized selections cannot be edited without an explicit reopen or audit entry. Finalized rounds contribute to season/league-season fairness calculations.
Finalized matches and rounds can be un-finalized to revert selections back to DRAFT for recalculation. Un-finalization reverts Selection.status from FINALIZED to DRAFT, clears ruleConfigVersion and overrideReason, reverts MovementLedger.isDraft from false to true, and re-derives round status from plan integrity signals. Per-match un-finalization keeps the round as FINALIZED if other matches are still finalized. When all matches in a round are un-finalized, the round status reverts based on its plan integrity signals. Un-finalization requires confirmation and is not silent.
The season overview (/season) is the fairness control surface for the league season. It helps the coach understand whether player load, support burden, development exposure, drops, and movement are fair across the season.
Primary view: Player × round matrix. Each row is a player, each column is a round. Cells show the role (Core, Support, Development, Squad repair) and team for that round. Summary columns show rounds played, core matches, support count, development count, additional actual appearances, drops, and plan integrity signal count.
Finalized only vs. Include drafts. The coach can toggle between finalized-only history and finalized-plus-draft planning. Draft selections are always visually distinct from finalized history — they are never mixed without clear labeling. Unavailable rounds do not count as fairness debt. Additional actual appearances from post-match reports are tracked separately from planned selections.
Movement path summary. A secondary view shows team-to-team movement totals: source team, target team, role, count, unique players, last used, and plan integrity signals. Each path row is drillable.
Player drill-down. Clicking a player shows their movement timeline across rounds: round, date, team, role, draft/finalized state, and explanation. The timeline is scoped to the selected league season.
Plan integrity signals. The overview generates signals such as high support burden, low development exposure, repeated additional actual appearances, consecutive movement, and disproportionate team support. Each signal includes category (Blocked/Decision required/Planning note), affected player/team/path, reason, drill-down link, and whether it is based on finalized-only or draft-included data.
Season export. The coach can export finalized match data and season statistics from the season overview page. Available formats: CSV, JSON, TXT, Markdown. Available visibility modes: coach (includes roles, plan integrity signals, movement paths, explanations, override reasons) and parent (hides internal planning tags and judgement, uses neutral language such as rotation, suitable challenge, team balance, availability, or development opportunity). Coach export includes selection details per match, movement rows with from/to team and role, and per-player statistics (rounds played, core matches, support matches, development matches, squad repair, additional actual appearances). Parent export includes per-selection rows with round, date, team, home/away, opponent, and player position, movement direction without team names or role labels, and per-player statistics with rounds played.
- Next.js 16 App Router (Turbopack)
- TypeScript
- Tailwind CSS
- Prisma
- PostgreSQL (local via Docker Compose or Neon)
- Auth.js (Google OAuth, email allowlist)
Matchboard is a private coaching app. Access is restricted to authenticated coaches on an email allowlist. Internal planning notes, readiness signals, feedback, and selection reasoning remain private coach-facing data by default.
- Users must authenticate (Google OAuth) before accessing any app data
- Access is controlled by
ALLOWED_COACH_EMAILS— a comma-separated list of coach email addresses - No public signup exists or should be added unless explicitly requested
- Authenticated users not on the allowlist see an access-denied page
- All server actions and API routes enforce authorization server-side
- UI-only protection is insufficient — every mutation and data read requires a verified coach session
- Database access is server-side only — no direct client database access
- Readiness signals, coaching intent, matchday responsibilities, execution feedback, and internal explanations must not appear in parent-facing exports
- Player names and personal data must not be sent to external AI services
- Git
- Node.js 22 LTS recommended (minimum 20.9.0 for Next.js 16)
git clone <your-repo-url>
cd matchboard
npm installnpm run docker:upThis starts a local Postgres container via Docker Compose on localhost:5432.
cp .env.example .envDefault .env points at local Docker Compose Postgres:
DATABASE_URL="postgresql://matchboard:matchboard@localhost:5432/matchboard?schema=public"
DIRECT_URL="postgresql://matchboard:matchboard@localhost:5432/matchboard?schema=public"
TEST_DATABASE_URL="postgresql://matchboard:matchboard@localhost:5432/matchboard_test?schema=public"For production, switch to Neon Postgres pooled/direct URLs (see .env.example).
Do not commit .env or any database credentials.
-
Generate an auth secret:
npx auth secret
Copy the output to
AUTH_SECRETin your.env. -
Create Google OAuth credentials:
- Go to Google Cloud Console
- Create a new project (or use an existing one)
- Go to APIs & Services → Credentials → Create Credentials → OAuth client ID
- Application type: Web application
- Authorized JavaScript origins:
http://localhost:3333 - Authorized redirect URIs:
http://localhost:3333/api/auth/callback/google - Copy the Client ID to
AUTH_GOOGLE_IDand Client Secret toAUTH_GOOGLE_SECRETin.env
-
Add your email to the allowlist:
ALLOWED_COACH_EMAILS="you@example.com"
-
Set the auth base URL:
AUTH_URL="http://localhost:3333"
npm run db:generate # Generate Prisma client into src/generated/prisma
npm run db:migrate # Apply schema migrationsFor active schema development:
npm run db:migrate:dev # Create and apply a new migrationnpm run db:seed:demoDemo seed creates fake players, teams, rotation paths, and match rounds. Never replace it with real player data.
npm run devRuns on http://localhost:3333.
| Command | Purpose |
|---|---|
npm run dev |
Start dev server on port 3333 |
npm run build |
Production build |
npm run lint |
Lint source files |
npm run test |
Run test suite (Vitest) |
npm run test:watch |
Run tests in watch mode |
npm run db:generate |
Generate Prisma client |
npm run db:migrate |
Apply schema migrations |
npm run db:migrate:dev |
Create and apply a new migration |
npm run db:seed:demo |
Seed fake demo data |
npm run db:export |
Export all app state to JSON |
npm run db:import |
Import app state from JSON export |
npm run docker:up |
Start local Postgres via Docker Compose |
npm run docker:down |
Stop local Postgres |
npm run docker:reset |
Reset local Postgres (destroys data) |
features/matchboard.feature— behavioral source of truth for all selection logic, rules, and domain behaviorAGENTS.md— coding agent instructions, workflow, style guide, and architecture constraintsdocs/domain/source-of-truth-register.md— canonical data sources for every important fact
If code and the Gherkin feature file disagree, the feature file wins.
When workflow or UX semantics change, update features/matchboard.feature, AGENTS.md, and README.md before implementing. Do not implement product-shape changes before aligning supporting docs.
| Fact | Canonical source |
|---|---|
| Played appearance | PRESENT actual participation in completed reports only |
| Player goals | Goal events in reported/locked reports |
| Player assists | MatchReportPlayerStat.assists in reported/locked reports |
| Planned player did not play | MatchReportAbsence with structured reason |
| Intended match opportunity | Selection (planned, not actual) |
| Matchday participation reality | Post-match report records |
| Current plan integrity | Derived live from computeRoundPlanIntegrity |
Rules:
UNKNOWNattendance does not count as played and blocks report completion.MatchReportPlayerStat.goalsis a compatibility field, not independent goal truth.- Draft and finalized planned selections do not count as actual played statistics.
- Never infer scorers from final score.
- Never infer historical attendance during reconciliation.
- Planned players who did not play require structured absence before report completion.
Teams, Players, and Matches are setup registries — dense, table-first data views for efficient entry. Each registry has a dedicated create route, prominent Create actions, and actionable empty states. Create buttons must never be dead links.
- Teams (
/teams): dense table with core player count, squad limits, support priority. Create at/teams/new. - Players (
/players): dense table with name, core team, position, availability. Create at/players/new. Requires at least one team. - Fixtures (
/fixtures): match registry with round hierarchy, status, and actions. Match creation at/matches/new. Requires at least one team.
Round selection (/rounds) remains workflow-first and uses cards, boards, and panels — not tables.
Empty states must be actionable:
- "No teams yet. Create a team." → direct link to
/teams/new - "Create a team first." (on Players when no teams) → direct link to
/teams/new - "No players yet. Create a player." → direct link to
/players/new - "No matches yet. Create a match." → direct link to
/matches/new
The Teams page is a lightweight directory. It shows each team with core player count, squad limits, support priority, active movement paths, and current league season burden. Each team links to its detail page.
The all-teams page must not become a catch-all dashboard. It must not show squad rosters inline. Detailed team work happens on the team-specific detail page.
The team detail page is the primary team workspace. It answers:
- Who belongs to this team
- Who is available
- Who is selected this round
- Who is moving out as support
- Who is moving in as support/squad repair/development
- Whether the team is short
- What plan integrity signals exist for this team
- What the team's movement and fairness situation looks like
Team detail sections:
- Team header — name, squad limits (target, minimum, maximum), minimum core, support priority
- Team summary strip — current round status, core count, sent as support count, received support/squad repair/development counts, plan integrity signal count
- Squad tab — core roster grouped by planning status (core regulars, support candidates, development candidates, non-rotatable, reduced match load, availability problems)
- Current Round tab — who is selected, sent, received, dropped for the active round, with selection reason and movement language
- Movement tab — movement history across rounds (sent as support, received support, received squad repair, received development)
- Movement candidates tab — incoming and outgoing movement candidates with create, pause, reactivate, and delete actions; drift and review overdue indicators
- History tab — finalized rounds for this team with role breakdown
- Rules/Links tab — rotation paths involving this team, squad size config, support priority, link to Rules page
Use neutral coaching language. Never use labels that imply permanent negative judgment.
| Concept | Use | Never use |
|---|---|---|
| Player sent to another team for support | Sent as support | Demoted, benched, punished, failed |
| Player received from another team | Received support, received squad repair, received development | Promoted, upgraded, reward |
| Player not selected for a round | Dropped, not selected this round | Benched, failed, weak player |
| Player moved for development | Development movement, development rotation | Promoted, rewarded, upgraded |
| Player filling a gap | Squad repair, cover, repair after support | Replacement, substitute |
| Team with fewer players than target | Short, below target | Weak team, B-team, reserve team |
| Team donating players | Donor team, support source | Stronger team, higher team |
| Team receiving players | Receiving team, support target | Weaker team, lower team |
Note: BACKFILL remains the internal code role and rotation path role. Use "squad repair" in all user-facing UI and documentation.
The round-level selection engine runs in this order:
- Per-match core selection (
deferRotationmode, fills onlyminCorePlayers) - Round-level required support resolution (
resolveRoundSupport) - Cross-match conflict resolution (
resolveRoundConflicts) - Development routing (
routeCoreMatchDrops) - Squad repair — repairing teams weakened by support movement
- Post-pipeline validation and plan integrity signal persistence
Key rules enforced by the engine:
- RotationPath is the single source of truth — non-core movement requires a valid directed path for the exact role; legacy relationship tables must not drive eligibility
- Paths are role-specific — a SUPPORT path only authorizes SUPPORT movement, not DEVELOPMENT or BACKFILL (and likewise for each role)
- Team support is priority 1 — required support must be fulfilled before development, fairness, or cosmetic balancing
- Support priority is ascending — lower number = higher priority (1 resolved before 2)
- Squad repair follows strict priority order — (1) own-core player on different date, (2) players from teams with active DEVELOPMENT or SUPPORT rotation path to receiving team where nonRotatable=false, (3) players with active SUPPORT or BACKFILL rotation path where nonRotatable=false
- Non-rotatable players are never used as generic squad repair
- Invalid path eligibility is a hard eligibility problem — not a ranking problem. Fairness scoring cannot make an invalid path valid.
- Same-round player uniqueness is the default — a player must not be planned for two matches in the same round. Controlled double-load has been removed from generation. Actual double-load from post-match reports is tracked through effective participation.
- Target squad size is a planning target, not a hard cap — teams may exceed target up to maxSquadSize. Below target but above minAcceptedSquadSize generates a Planning note only.
- Plan integrity signals are generated and persisted when support or squad repair cannot be fulfilled — the team is never silently weakened
- Donor teams must not fall below
minCorePlayersduring support resolution - Rotation paths are directional — movement cannot happen without an explicit path in the correct direction
- Each player can only appear once per match round unless actual participation differs from planned selection
- Draft selections are editable — manual edits use same domain validation as automatic generation
- Finalized selections are immutable — manual overrides require an audit reason
- Manual override requires reason and must appear in finalization summary
- Below target but above minimum generates a Planning note, not a Blocked condition
- Plan integrity signals are generated and persisted — the team is never silently weakened
Populate all generates drafts for all non-finalized rounds in the active league season:
- Calls
generateMatchRoundfor each round in chronological order - Uses round-level orchestration (not match-by-match)
- Does not finalize any round
- Skips already-finalized rounds
- Persists plan integrity signals per round after generation
- Draft selections from earlier rounds may be used as provisional planning context for later rounds
- On partial failure, successful round generations are kept and failures are reported
| Path | Purpose |
|---|---|
src/lib/selection/ |
Selection engine, round-level orchestrator, support, routing, squad repair |
src/lib/rounds/ |
Round-level engagement computation and override validation |
src/lib/seasons/ |
LeagueSeason part (SPRING/FALL), year, label, and date range derivation |
src/lib/rules/ |
Rule configuration loading and validation |
src/lib/ |
DB client, shared utilities, player metrics, date helpers |
src/app/ |
Next.js App Router pages, layouts, server actions, API routes |
src/components/ |
Shared React components |
features/ |
Gherkin feature file |
RotationPath is the single source of truth for automatic non-core player movement. Movement rules:
- A player may only be selected outside their core team when an active directed RotationPath exists from core team to target team for the exact role being assigned, unless a manual override with reason is used
- Each RotationPath authorizes exactly one role: SUPPORT, DEVELOPMENT, or BACKFILL
- A SUPPORT path permits only SUPPORT movement — not DEVELOPMENT or BACKFILL
- A DEVELOPMENT path permits only DEVELOPMENT movement — not SUPPORT or BACKFILL
- A BACKFILL path permits only BACKFILL movement — not SUPPORT or DEVELOPMENT
- Paths are directional: from_team → to_team only. The reverse direction requires a separate path
- No configured path means no automatic non-core selection
- Fairness scoring cannot make an invalid path valid
- Non-rotatable players cannot be automatically selected for any non-core role
- Manual override may bypass path checks but must record reason
- The legacy TeamSupportSource and TeamDevelopmentSource relationship tables must not drive selection eligibility — they are scheduled for removal
- Team: configurable squad limits (
targetSquadSize,minAcceptedSquadSize,maxSquadSize), support settings, development slots, support priority rank (1 is highest) - RotationPath: directed edges between teams with role (SUPPORT, BACKFILL, DEVELOPMENT), cooldown, and count targets
- MovementCandidate: per-player per-path preference record with role (SUPPORT, DEVELOPMENT), status (ACTIVE, PAUSED), structured rationale category, optional review date, and optional note. Unique constraint on
[playerId, rotationPathId, role]. Active candidates receive +12 scoring bonus in selection engine. Coach-facing only — never in parent-facing exports. - Selection: per-player per-match record with role (CORE, SUPPORT, DEVELOPMENT, SQUAD_REPAIR), status (DRAFT/FINALIZED), overrideReasonCategory (enum), overrideReasonDetail (free text), and structured explanation JSON. BACKFILL remains in the Prisma enum for backward compatibility of historical data and manual overrides. New generation produces SUPPORT with squad repair explanation codes for squad repair, not BACKFILL. DOUBLE_LOAD is not a valid role value for new generation. The
controlledDoubleLoadfield is legacy — no newtruevalues are written by the generation engine. Actual double-load from post-match reports is tracked through effective participation, not viacontrolledDoubleLoad. Additional actual appearances from post-match reports are recorded separately as unplanned participation and do not mutate finalized planned selections. - MovementLedger: mandatory record for every non-core player movement. Created during draft generation, flipped from isDraft=true to isDraft=false during finalization. Support, development, and squad repair from another team all create ledger entries. The movement ledger is the authoritative record of player movement — the export must never show empty movements when non-core selections exist.
- MatchRound: weekly planning unit — selections are generated and validated per round, not per match in isolation
- LeagueSeason: bounded spring/autumn operational window with auto-derived
part(SPRING or FALL) and belonging to a Season with ayearfield. The internal model is LeagueSeason (was PlanningPeriod). User-facing text uses "League season" or "Season", never "Planning period" or "Phase". - Warning: per-round signals with severity (HARD_BLOCK, REQUIRES_OVERRIDE, WARNING, SCORING_PREFERENCE), persisted to database. Current active integrity is derived from the current editable draft — recalculation yielding zero signals clears stale rows. The UI displays these as Blocked, Decision required, or Planning note based on the visible signal model. SCORING_PREFERENCE is explanation only and is never persisted as an active issue. Planning notes never create active work items or finalisation requirements.
This repo is intended to stay safe for a public remote:
- Never commit real player names or private roster data
- Never commit database credentials or Neon connection strings
- Never commit AUTH_SECRET, AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET, or any auth credentials
- Never prefix secrets with NEXT_PUBLIC_ (they would be exposed to the browser)
- Never commit
.envor machine-specific secrets - Keep imported or exported real data in ignored local directories only
- Demo and example data committed to the repo must be fake
- Seed data uses fictional player names (P1, P2, etc.) and team names (Team A, Team B, Team C)
Matchboard is deployed to Vercel with Neon PostgreSQL as the production database. It is a hosted private web app with PostgreSQL backend persistence. SQLite is not used for production persistence — only PostgreSQL is supported.
Do not deploy without auth enabled. All server actions and API routes must enforce requireCoachAccess().
- A Neon Postgres database (created and migrated)
- A Vercel account
- A Google Cloud project with OAuth credentials
- Node.js 22 LTS recommended
Set these in the Vercel project dashboard under Settings → Environment Variables. Do not commit these values to the repository.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Neon pooled connection string (hostname includes -pooler). Used by the runtime app for queries. |
DIRECT_URL |
Neon direct connection string (no -pooler). Used by Prisma CLI for migrations. |
AUTH_SECRET |
Generated with npx auth secret. Server-side only. |
AUTH_GOOGLE_ID |
Google OAuth client ID from Google Cloud Console. |
AUTH_GOOGLE_SECRET |
Google OAuth client secret from Google Cloud Console. |
AUTH_URL |
Your deployed URL (e.g. https://matchboard.vercel.app). Auth.js uses this for callbacks. |
ALLOWED_COACH_EMAILS |
Comma-separated list of coach email addresses. Only these users can access the app. |
Never prefix secrets with NEXT_PUBLIC_ — they would be exposed to the browser.
- The runtime app uses
DATABASE_URL(pooled connection) for queries via@prisma/adapter-neonor@prisma/adapter-pg. - Prisma CLI (migrations, generate) uses
DIRECT_URL(direct connection) configured inprisma.config.ts. - The Prisma schema (
prisma/schema.prisma) declaresprovider = "postgresql"with no inlineurl— the URL is provided viaprisma.config.tsand environment variables. src/lib/db.tsauto-detects Neon vs. local Postgres from the connection string and uses the appropriate adapter.
- Production migrations must be run deliberately from a local machine targeting the Neon database using
DIRECT_URL. Do not run migrations as part of the Vercel build process. - Before first deployment, run:
npx prisma migrate deploywithDIRECT_URLpointing to your Neon database. - The
postinstallscript runsprisma generate(not migrations). - Never use
prisma migrate devagainst production. - If production schema already matches the latest migration, no action is needed.
In Google Cloud Console → APIs & Services → Credentials, configure:
Authorized JavaScript origins:
http://localhost:3333(local development)https://your-domain.vercel.app(production)
Authorized redirect URIs:
http://localhost:3333/api/auth/callback/google(local development)https://your-domain.vercel.app/api/auth/callback/google(production)
If using Vercel preview deployments, add the preview URL as well.
After changing the production URL, update AUTH_URL in Vercel environment variables and redeploy.
- Push the repository to GitHub.
- In Vercel dashboard: New Project → import the GitHub repository.
- Framework preset: Next.js (should be auto-detected).
- Build command:
next build(Vercel default). - Install command:
npm install(Vercel default). - Output directory: default (
.next). - Node.js version: 22.x (set in Project Settings → Node.js Version).
- Add all seven environment variables from section 2.
- Deploy.
Do not commit .vercel/ — it is in .gitignore.
After deployment, verify:
- Production URL loads and shows the sign-in page
- Sign-in page is themed (dark background) and does not show sidebar/topbar
- Unauthorized users (not on allowlist) are denied access
- An allowlisted coach can sign in via Google
- Protected pages render after authentication
- Neon database connection works (create a team to test)
-
/api/healthreturns{ ok: true }with no business data - No secrets visible in browser source or Vercel logs
- No protected data accessible without authentication
If a deployment has issues:
- Rollback: In Vercel dashboard → Deployments → find the last known-good deployment → Promote to Production.
- Disable access: Set
ALLOWED_COACH_EMAILSto an empty string or remove it — all app access will be denied.
- Rate limiting is in-memory only. It resets on server restart and does not work across multiple Vercel instances. For production, consider a Redis-backed rate limiter.
- The
/api/healthendpoint is public and returns{ ok: true }only — no business data is exposed. - All other API routes and server actions enforce
requireCoachAccess(). - Never expose
DATABASE_URL,AUTH_SECRET, or other secrets asNEXT_PUBLIC_*variables. .envis for local development only and must never be committed..vercel/is local build metadata and must never be committed.
Matchboard supports temporary event squad planning for cups, tournaments, friendly days, and similar events. Event squads are separate from league match-round planning.
- Event: A temporary planning context (cup, tournament, friendly day) with its own player pool, availability, and squad generation
- Event squad: A temporary squad within an event — NOT a league Team row
- Competitive squad: Built from formation/role needs first, NOT a simple top-N-by-overall ranking
- Balanced remainder: Remaining players deliberately distributed, NOT "leftover" or "B team"
- Not rated: Null player attributes are uncertainty, NOT zero ability or max skill
| Concept | Use | Never use |
|---|---|---|
| Cup/tournament/friendly-day context | Event | Tournament mode, Cup mode |
| Temporary squad | Event squad | Temporary team, Scratch team |
| Strongest squad | Competitive squad | Topped team, A team, Best team |
| Remaining players | Balanced remainder | Leftover players, B team |
| Player with null ratings | Not rated | Unrated, Default max |
Event squad generation is entirely separate from league planning:
- Does NOT create Selection, MatchRound, or normal Availability rows
- Does NOT affect league fairness metrics
- Does NOT mutate finalized match history
- Event squad assignments do NOT count as league appearances
- May READ existing players, attributes, positions, formations, and readiness for context
- Null = "Not rated" — never displayed as 0 or max skill
- Scale 1–5: 1 = needs support, 2 = developing, 3 = steady, 4 = strong, 5 = standout in this group
- Composite attributes for event generation: overallLevel, defending, attacking, gameUnderstanding, intensity, teamplay, goalkeeperAbility
- Goals, assists, and post-match stats must NEVER directly become skill ratings
- Ratings are internal coach-facing planning context, not parent-facing or public
- All balanced: Distribute all players evenly across squads
- One competitive + balanced remainder: Build one tactic-aware competitive squad, then balance the rest
- Manual seed + auto-balance: Coach locks players, generator distributes the rest
/events— event list page (secondary destination)/events/new— create event/events/[eventId]— event detail/planning with tabs: Overview, Squads, Player pool, Matches
Events do not appear as primary sidebar items.
Players can temporarily help other event squads when their own squad is not playing at the same time.
- Match duration: Set on the event detail overview (inline edit). Required before support planning.
- Overlap rule:
a.startsAt < b.endsAt && b.startsAt < a.endsAt— exact boundary times do NOT overlap. - Eligibility: Player must be in a different squad, their own squad must not be playing at the same time, and they must not be UNAVAILABLE or WITHDRAWN.
- Guardrail enforcement: Helper eligibility is calculated server-side using
getSupportCandidatesForEventMatch. The UI fetches candidates from this action. The add-helper server action independently recalculates eligibility before persisting. - UI separation: Available helpers and unavailable helpers are shown in separate sections. Unavailable helpers display the specific reason (overlapping match, unavailable, already helping, etc.).
- Conflict detection: Dynamic at query time. Shows warnings when match times change after assignment, player is removed from source squad, or availability changes.
- Recalculation: Editing a match time or squad triggers conflict recalculation for all support assignments in the event.
- Planned roles: Optional — GK cover, Defender cover, Midfield cover, Forward cover, General cover.
- Post-match integration: Support players appear in reports with "Planned helper from {squad name}" role label.
Each event squad match can be edited after creation.
- Editable fields: Opponent name, squad, date/time, category (Cup/Other), location, notes.
- Squad changes: Must belong to the same event. Recalculates support availability and conflicts.
- Time changes: Recalculates derived end time and support conflicts.
- Completed report restrictions: Time and squad changes are blocked for matches with completed (non-DRAFT) reports. Category changes are also blocked. Opponent, location, and notes may still be editable.
- Category validation: Only CUP and OTHER are valid for event matches. LEAGUE is rejected.
Coaches can export a complete event overview as an Excel workbook (.xlsx) from the event detail page.
- Export button: Available on the event detail page header. Downloads the file immediately.
- Filename: Based on the event name and start date (e.g.,
Slemmestad Cup 2026-07-08.xlsx). - Overview sheet: Event name, type (friendly label), game format, match duration, squad count, match count, export timestamp.
- Squads sheet: One row per player per squad — squad name, player name, position. Does not expose ratings or internal IDs.
- Match plan sheet: One row per match — squad, opponent, category (friendly label), date, start/end time, status (friendly label), planned helpers.
- Support plan sheet: One row per planned helper — match times, target squad, opponent, helper name, source squad, planned role, conflict status.
- Support load sheet: One row per helper — name, source squad, number of support matches, conflict count.
- Conflicts sheet: Lists active support conflicts with match context and reason, or "No support conflicts" if none.
- Labels: Uses friendly labels throughout (7-a-side not SEVEN_A_SIDE, Cup not CUP, Scheduled not SCHEDULED, Not rated not null).
- Missing duration: Shows "Not set" and "Duration not set" when match duration is not configured.
- Authentication: Required. Unauthenticated requests are rejected.
The event detail page provides a Player pool tab (not "Availability") with:
- Add players: Search and select from all active players not yet in the pool. Added players default to AVAILABLE status.
- Manage pool: View all players in the pool, change availability status, remove players from the pool (which also removes any squad assignment).
- Manual squad assignment: Unassigned players in the Squads tab can be assigned to any squad via dropdown. Players in squads can be unassigned back to the pool without removing from the pool.
- Squad generation requires at least one AVAILABLE player in the event pool
- An empty pool shows a clear message directing the coach to add players first
- No available players shows a clear message directing the coach to mark players as Available
- Prefer small files and clear names over short names
- Return explanation objects from selection logic — the app must never behave like a black box
- Validate inputs at boundaries (server actions, API routes)
- Keep UI, rules config, and selection engine separate
- Prefer explicit domain code over generic abstractions
- Keep the UI calm and operational — tables are supporting elements, not primary workflows
- Auth is required: every server action and route must enforce
requireCoachAccess()
Agents should not work directly on main unless explicitly instructed.
- Start from the latest
main - Create one feature branch per task or fix
- Keep branches narrowly scoped
- Prefer names like
feature/player-detail-navigation,fix/finalize-selection-history,chore/readme-cleanup
Use Conventional Commits:
feat: add next-player navigation on player detail page
fix: preserve finalized selection history on recalculation
docs: rewrite local setup and agent workflow guide
refactor: move selection filtering into domain helpers
test: cover support-team eligibility rules
chore: update deployment config
Keep each commit focused on one logical change. Do not mix unrelated cleanup with behavior changes.
Each PR should:
- Stay focused on one change set
- Explain the problem and chosen implementation
- Call out any schema, migration, or rule changes
- Mention any behavior changes against
features/matchboard.feature - List verification performed (lint, typecheck, manual checks)
- Note anything not verified