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:
- Test the hypothesis that Go + HTMX produces more reliable AI-generated code
- Extract Forge conventions from a real, working application
- 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) → TournamentvalidateTournamentInput(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) → TeaminvitePlayer(teamId, email) → InvitationacceptInvitation(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) → ValidationResultgenerateBracket(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) → MatchadvanceWinner(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
- Bracket visualization: Build custom or use existing library?
- Seeding: Manual only, or support auto-seeding algorithms?
- Check-in flow: Required before tournament starts, or optional?
- Game-specific data: Store in JSON column, or separate tables per game?
References
- Forge DECISIONS.md - Stack choices
- Forge PROGRESS.md - Hypothesis validation
- Challonge - Competitive reference
- Start.gg - Feature reference (not scope)