Project Doc

rally-hq Specification

Proof-of-concept app specification

Updated: December 2025 Source: RALLY-HQ.md

rally-hq Specification

Status: Draft Purpose: Define scope for rebuilding rally-hq with Forge conventions


Overview

rally-hq is an existing tournament management application currently built with React and Next.js. For the Forge project, we are rebuilding it from scratch using the Forge stack (Go + HTMX + Svelte islands) to:

  1. Test the hypothesis that Go + HTMX produces more reliable AI-generated code
  2. Extract Forge conventions from a real, working application
  3. Compare development experience between Next.js and the primitive-first approach

Existing app: React + Next.js (functional, production-ready) Forge rebuild: Go + HTMX + Svelte islands (proof-of-concept)

The functional requirements below are extracted from the existing working application.

Domain: Esports/gaming tournament organization Users: Tournament organizers, team captains, players Scale: Single-tenant, small-to-medium tournaments (8-64 teams)


MVP Scope

Core Features

Feature Priority Description
Create tournament P0 Organizer creates tournament with name, game, format
Team registration P0 Teams register with roster of players
Bracket generation P0 Auto-generate single/double elimination bracket
Match scoring P0 Record match results, advance winners
Tournament status P1 View bracket, standings, schedule
Player profiles P2 Basic player info, tournament history

Out of Scope (MVP)

  • Multi-tenant / SaaS features
  • Payment processing
  • Live streaming integration
  • Advanced formats (Swiss, round-robin)
  • Mobile app
  • Real-time spectator updates

Data Model

Core Entities

Tournament
├── id: uuid
├── name: string
├── game: string
├── format: 'single_elimination' | 'double_elimination'
├── status: 'draft' | 'registration' | 'in_progress' | 'completed'
├── max_teams: number
├── created_at: timestamp
└── organizer_id: uuid (User)

Team
├── id: uuid
├── name: string
├── tournament_id: uuid
├── captain_id: uuid (User)
├── status: 'registered' | 'checked_in' | 'eliminated' | 'winner'
└── seed: number (nullable)

Player
├── id: uuid
├── user_id: uuid
├── team_id: uuid
├── role: string (game-specific)
└── joined_at: timestamp

Match
├── id: uuid
├── tournament_id: uuid
├── round: number
├── position: number (bracket position)
├── team_a_id: uuid (nullable)
├── team_b_id: uuid (nullable)
├── winner_id: uuid (nullable)
├── score_a: number (nullable)
├── score_b: number (nullable)
├── status: 'pending' | 'in_progress' | 'completed'
└── scheduled_at: timestamp (nullable)

User
├── id: uuid (Supabase Auth)
├── email: string
├── display_name: string
└── created_at: timestamp

Relationships

User (1) ──────── (N) Tournament (as organizer)
User (1) ──────── (N) Team (as captain)
User (1) ──────── (N) Player
Team (1) ──────── (N) Player
Tournament (1) ── (N) Team
Tournament (1) ── (N) Match
Team (N) ──────── (N) Match (as team_a or team_b)

User Flows

Flow 1: Create Tournament

Organizer → Dashboard → "Create Tournament"
         → Form: name, game, format, max_teams
         → Tournament created (status: draft)
         → Redirect to tournament management

Primitives needed:

  • createTournament(input) → Tournament
  • validateTournamentInput(input) → ValidationResult

Flow 2: Team Registration

Captain → Browse tournaments → Select tournament
       → "Register Team" → Form: team name
       → Add players (email invite or search)
       → Team registered (status: registered)

Primitives needed:

  • registerTeam(tournamentId, teamData) → Team
  • invitePlayer(teamId, email) → Invitation
  • acceptInvitation(invitationId, userId) → Player

Flow 3: Start Tournament

Organizer → Tournament management → "Start Tournament"
         → System validates: enough teams, all checked in
         → Bracket generated
         → Tournament status → in_progress

Primitives needed:

  • validateTournamentStart(tournamentId) → ValidationResult
  • generateBracket(tournamentId, teams[]) → Match[]
  • startTournament(tournamentId) → Tournament

Flow 4: Record Match Result

Organizer → Match detail → "Record Result"
         → Form: score_a, score_b
         → Winner determined → Match status: completed
         → Winner advances to next match

Primitives needed:

  • recordMatchResult(matchId, scoreA, scoreB) → Match
  • advanceWinner(matchId) → Match (next match updated)
  • checkTournamentCompletion(tournamentId) → boolean

Workflows (State Machines)

Tournament Lifecycle

States: draft → registration → in_progress → completed

Transitions:
  draft → registration
    trigger: organizer publishes
    validation: has name, game, format, max_teams

  registration → in_progress
    trigger: organizer starts tournament
    validation: min 2 teams, all checked in

  in_progress → completed
    trigger: final match completed
    validation: winner determined

Match Lifecycle

States: pending → in_progress → completed

Transitions:
  pending → in_progress
    trigger: both teams assigned AND scheduled_at reached
    validation: team_a and team_b not null

  in_progress → completed
    trigger: result recorded
    validation: scores entered, winner determined

Component Registry (Initial)

Component Description Variants
TournamentCard Display tournament summary compact, detailed
TeamCard Display team with roster compact, detailed
BracketView Visualize elimination bracket single, double
MatchCard Display match with teams/scores pending, live, completed
PlayerBadge Display player avatar/name small, medium
StatusBadge Display status with color draft, active, completed
ScoreInput Input for match scores standard
RegistrationForm Multi-step team registration standard

Technical Requirements

Authentication

  • Session-based auth (Go stdlib)
  • Roles: organizer, captain, player (middleware-based)
  • No OAuth in MVP (add later)

Database

  • PostgreSQL 16 (Supabase or Neon managed)
  • sqlc for type-safe queries
  • SQL migrations (golang-migrate)

Real-time

  • SSE (Server-Sent Events) via HTMX hx-ext="sse"
  • Live score updates, bracket progression
  • Browser-native EventSource API

Deployment

  • Fly.io (Go-native, edge deploys)
  • Single binary, Docker container
  • Environment: production only (no staging in MVP)

Testing Strategy

Unit Tests (Go testing)

  • All handlers and services have unit tests
  • Table-driven tests for comprehensive coverage

Property-Based Tests (rapid)

  • Bracket generation produces valid brackets
  • Score recording maintains bracket integrity
  • Tournament state transitions are valid

E2E Tests (Playwright)

  • Happy path: create tournament → register teams → play matches → winner
  • Add after MVP core is stable

Island Tests (Vitest)

  • Svelte island components tested separately
  • Interaction tests for drag-drop, rich editors

Milestones

Milestone Definition of Done
M1: Project Setup Go + HTMX + Templ + Postgres connected, deployed to Fly.io
M2: Tournament CRUD Create/read/update tournaments, basic HTMX UI
M3: Team Registration Teams can register, players can join
M4: Bracket Generation Single elimination bracket generated (Svelte island)
M5: Match Scoring Record results, advance winners, SSE live updates
M6: MVP Complete Full flow works end-to-end, tests passing

Open Questions

  1. Bracket visualization: Build custom or use existing library?
  2. Seeding: Manual only, or support auto-seeding algorithms?
  3. Check-in flow: Required before tournament starts, or optional?
  4. Game-specific data: Store in JSON column, or separate tables per game?

References