24 KiB
GridPilot Alpha Development Plan
Overview
Mission: Prove automation + unified management value proposition through functional alpha.
Scope: In-memory web app demonstrating core league management workflows integrated with companion automation.
Timeline: Alpha validates technical approach; production follows with persistent infrastructure.
Value Proof:
- Automation reduces admin workload (companion integration)
- Unified platform consolidates fragmented tools (web app)
- Clean Architecture enables rapid iteration (in-memory → production swap)
Technical Approach
Architecture Strategy
Production domain logic, swappable infrastructure:
Domain (Production) ← Business rules, invariants
Application (Production) ← Use cases, orchestration
Infrastructure (Swappable) ← In-memory adapters for alpha
Presentation (Production) ← Next.js UI components
Alpha = Real code + Fake persistence
- Domain entities enforce actual business rules
- Application use cases implement real workflows
- Infrastructure uses in-memory adapters (no database)
- Presentation renders production-ready UI
Post-Alpha Migration:
// Alpha
const driverRepo = new InMemoryDriverRepository();
// Production
const driverRepo = new PostgresDriverRepository(pool);
Same ports, same use cases, different adapters.
Mode System Design
Current: pre-launch | post-launch
Alpha: pre-launch | alpha
Mode Behavior:
pre-launch: Landing page only (current state)alpha: Full app UI with in-memory data (no auth, no persistence)
Implementation:
// lib/mode.ts
export type AppMode = 'pre-launch' | 'alpha';
// Middleware gates
if (mode === 'alpha') {
// Enable /dashboard, /leagues, /profile routes
// Inject in-memory repositories
}
Environment Control:
GRIDPILOT_MODE=alpha npm run dev
Features
F1: Driver Profile Creation
User Story: Driver creates single profile to participate in leagues.
Acceptance Criteria:
- Driver enters: name, racing number, iRacing ID
- System validates: unique number per league, valid ID format
- Profile displays: basic info, empty race history
- Single profile per user (alpha constraint)
Domain Model:
class Driver {
id: DriverId
name: string
racingNumber: number
iRacingId: string
createdAt: Date
}
UI Flow:
/profile/createform- Validation feedback (inline)
- Success → redirect to
/dashboard - Profile visible at
/profile
F2: League Management
User Story: Admin creates league, drivers join, schedule displays.
Acceptance Criteria:
- Admin creates league: name, description, rules
- System generates: league ID, creation timestamp
- Drivers browse available leagues
- Drivers join single league (alpha constraint)
- League page shows: roster, schedule placeholder, standings
Domain Model:
class League {
id: LeagueId
name: string
adminId: DriverId
description: string
rules: string
members: DriverId[]
createdAt: Date
}
UI Flow:
/leagues/create(admin only)/leagues(browse/join)/leagues/:id(league detail)- Join button → updates roster
- Leave button available
F3: Race Scheduling via Companion
User Story: Admin schedules race through companion, web app displays schedule.
Acceptance Criteria:
- Admin configures race in companion app
- Companion sends race metadata to web app (IPC/API stub)
- Web app displays: track, cars, date/time, session settings
- Schedule shows upcoming races
- Past races marked as completed
Domain Model:
class Race {
id: RaceId
leagueId: LeagueId
trackId: string
carIds: string[]
scheduledAt: Date
sessionName: string
status: 'scheduled' | 'completed' | 'cancelled'
}
Integration:
// Companion → Web App (simulated in alpha)
interface RaceScheduleRequest {
leagueId: string
sessionConfig: HostedSessionConfig
scheduledAt: Date
}
UI Flow:
/leagues/:id/scheduledisplays races- Each race shows: date, track, cars
- Status badge: upcoming/completed
- Click race → detail view
F4: Results Display (Manual Import)
User Story: Admin imports results CSV, system displays classification and updates standings.
Acceptance Criteria:
- Admin uploads iRacing results CSV
- System parses: positions, drivers, incidents, lap times
- Classification displays immediately
- Standings auto-calculate
- Results linked to scheduled race
Domain Model:
class RaceResult {
id: ResultId
raceId: RaceId
driverId: DriverId
position: number
incidents: number
fastestLap?: number
status: 'finished' | 'dnf' | 'dsq'
}
UI Flow:
/leagues/:id/races/:raceId/results/import- CSV upload field
- Preview parsed results
- Confirm → save
- Redirect to
/leagues/:id/races/:raceId/results
F5: Standings Calculation
User Story: System calculates driver and team standings from race results.
Acceptance Criteria:
- Points awarded per position (configurable preset)
- Standings update after each result import
- Display: position, driver, points, races
- Team standings aggregate member points
- Drop weeks not implemented (alpha limitation)
Domain Model:
class Standing {
leagueId: LeagueId
driverId: DriverId
position: number
points: number
racesCompleted: number
}
class PointsPreset {
name: string
pointsMap: Map<number, number> // position → points
}
Calculation:
class CalculateStandingsUseCase {
execute(leagueId: LeagueId): Standing[] {
const results = resultRepo.findByLeague(leagueId);
const points = applyPointsPreset(results);
return sortByPoints(points);
}
}
UI Flow:
/leagues/:id/standings(driver view)- Table: pos, driver, points, races
- Auto-updates after result import
- Team standings at
/leagues/:id/standings/teams
Data Model (In-Memory)
Core Entities
// Domain/entities/Driver.ts
class Driver {
id: DriverId
name: string
racingNumber: number
iRacingId: string
currentLeagueId?: LeagueId
createdAt: Date
}
// Domain/entities/League.ts
class League {
id: LeagueId
name: string
adminId: DriverId
description: string
rules: string
memberIds: DriverId[]
pointsPreset: PointsPreset
createdAt: Date
}
// Domain/entities/Race.ts
class Race {
id: RaceId
leagueId: LeagueId
trackId: string
carIds: string[]
scheduledAt: Date
sessionName: string
status: RaceStatus
hostedSessionId?: string // Links to companion
}
// Domain/entities/RaceResult.ts
class RaceResult {
id: ResultId
raceId: RaceId
driverId: DriverId
position: number
incidents: number
fastestLapTime?: number
status: ResultStatus
}
// Domain/entities/Standing.ts
class Standing {
leagueId: LeagueId
driverId: DriverId
position: number
points: number
racesCompleted: number
updatedAt: Date
}
Value Objects
// Domain/value-objects/DriverId.ts
class DriverId {
value: string
static generate(): DriverId
}
// Domain/value-objects/RaceStatus.ts
type RaceStatus = 'scheduled' | 'completed' | 'cancelled';
// Domain/value-objects/ResultStatus.ts
type ResultStatus = 'finished' | 'dnf' | 'dsq';
// Domain/value-objects/PointsPreset.ts
class PointsPreset {
name: string
pointsMap: Map<number, number>
static F1_2024 = new PointsPreset('F1 2024', new Map([
[1, 25], [2, 18], [3, 15], [4, 12], [5, 10],
[6, 8], [7, 6], [8, 4], [9, 2], [10, 1]
]));
}
UI/UX Flow
User Journey: Driver Registration → Race → Results
Step 1: Landing (Pre-Launch Mode)
- Visit
/ - See landing page
- Email signup (not functional in alpha)
Step 2: Switch to Alpha Mode
GRIDPILOT_MODE=alpha- Reload page
- Landing replaced with dashboard redirect
Step 3: Create Profile
- Visit
/profile/create - Enter: "John Doe", number 42, iRacing ID "123456"
- Submit → profile created
- Redirect
/dashboard
Step 4: Join League
- Visit
/leagues - See available leagues
- Click "European GT League"
- Review schedule, rules
- Click "Join League"
- Confirmation modal
- Success → league roster updated
Step 5: View Schedule
- Dashboard shows next race
- Visit
/leagues/:id/schedule - See upcoming: "Monza GP - March 15, 2025"
- Race details: GT3, 60min, 19:00 CET
Step 6: Race Day (External)
- Driver joins iRacing hosted session
- Companion automation created session
- Driver races (outside GridPilot)
Step 7: Results Import (Admin)
- Admin downloads iRacing CSV
- Visit
/leagues/:id/races/:raceId/results/import - Upload CSV
- Preview shows classification
- Confirm import
Step 8: View Results & Standings
- Driver visits
/leagues/:id/races/:raceId/results - Sees: 3rd place, 2x incidents, 1:42.5 fastest lap
- Click "Standings"
- Driver standings show: 15 points, 1 race
- Team standings aggregate teammates
Companion Integration
Race Scheduling Flow
Companion Side:
// Companion creates hosted session
const sessionConfig: HostedSessionConfig = {
sessionName: 'European GT - Monza GP',
trackId: 'monza-gp',
carIds: ['porsche-911-gt3-r'],
scheduledAt: new Date('2025-03-15T19:00:00Z'),
// ... full config
};
// Send to web app (alpha: IPC stub)
await webAppClient.scheduleRace({
leagueId: 'league-123',
sessionConfig,
});
Web App Side:
// Application/use-cases/ScheduleRaceUseCase.ts
class ScheduleRaceUseCase {
async execute(req: ScheduleRaceRequest): Promise<Race> {
// Validate league exists
const league = await leagueRepo.findById(req.leagueId);
// Create race entity
const race = Race.create({
leagueId: req.leagueId,
trackId: req.sessionConfig.trackId,
carIds: req.sessionConfig.carIds,
scheduledAt: req.scheduledAt,
sessionName: req.sessionConfig.sessionName,
status: 'scheduled',
});
// Persist (in-memory for alpha)
await raceRepo.save(race);
return race;
}
}
Alpha Implementation:
- Companion → Web App via simulated IPC
- Web app stores race in
InMemoryRaceRepository - Schedule view renders from repository
- No actual Playwright automation in web app
Post-Alpha:
- Companion → Web App via REST API
- Web app stores in PostgreSQL
- Companion polls for scheduled races
- Full automation execution
Result Import Flow
Manual Import (Alpha):
// Application/use-cases/ImportRaceResultsUseCase.ts
class ImportRaceResultsUseCase {
async execute(raceId: RaceId, csvData: string): Promise<RaceResult[]> {
// Parse CSV
const parsed = parseIRacingCSV(csvData);
// Validate race exists and is completed
const race = await raceRepo.findById(raceId);
// Map to domain entities
const results = parsed.map(row => RaceResult.create({
raceId,
driverId: findDriverByIRacingId(row.customerId),
position: row.finishPos,
incidents: row.incidents,
fastestLapTime: row.bestLapTime,
status: row.reasonOut ? 'dnf' : 'finished',
}));
// Persist
await resultRepo.saveMany(results);
// Trigger standings recalculation
await calculateStandingsUseCase.execute(race.leagueId);
return results;
}
}
Post-Alpha Automation:
- Web app calls iRacing API directly
- Automated result fetch after session end
- No CSV upload required
Testing Strategy
Unit Tests
Domain Layer:
describe('Driver', () => {
it('validates racing number uniqueness per league', () => {
// Test domain invariants
});
});
describe('Standing', () => {
it('calculates points from results', () => {
// Test scoring logic
});
});
describe('PointsPreset', () => {
it('applies F1 2024 points correctly', () => {
// Test point mapping
});
});
Application Layer:
describe('CreateDriverProfileUseCase', () => {
it('creates driver with valid data', async () => {
const repo = new InMemoryDriverRepository();
const useCase = new CreateDriverProfileUseCase(repo);
const driver = await useCase.execute({
name: 'John Doe',
racingNumber: 42,
iRacingId: '123456',
});
expect(driver.name).toBe('John Doe');
});
it('rejects duplicate racing number in same league', async () => {
// Test business rule enforcement
});
});
Integration Tests
Repository Layer:
describe('InMemoryDriverRepository', () => {
it('persists and retrieves driver', async () => {
const repo = new InMemoryDriverRepository();
const driver = Driver.create({...});
await repo.save(driver);
const found = await repo.findById(driver.id);
expect(found).toEqual(driver);
});
it('handles concurrent writes', async () => {
// Test in-memory consistency
});
});
Use Case Workflows:
describe('Race Result Import Workflow', () => {
it('imports CSV and updates standings', async () => {
// Setup: league, drivers, scheduled race
// Execute: import CSV
// Assert: results saved, standings updated
});
});
E2E Tests (Playwright)
User Flows:
test('driver can create profile and join league', async ({ page }) => {
await page.goto('/profile/create');
await page.fill('[name="name"]', 'John Doe');
await page.fill('[name="racingNumber"]', '42');
await page.fill('[name="iRacingId"]', '123456');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await page.goto('/leagues');
await page.click('text=European GT League');
await page.click('text=Join League');
await expect(page.locator('.roster')).toContainText('John Doe');
});
Integration with Companion:
test('admin schedules race via companion integration', async ({ page }) => {
// Simulate companion IPC call
await simulateCompanionScheduleRace({
leagueId: 'league-123',
sessionConfig: { /* ... */ },
});
// Verify web app displays scheduled race
await page.goto('/leagues/league-123/schedule');
await expect(page.locator('.race-card')).toContainText('Monza GP');
});
Success Metrics
Technical Validation
Architecture Proof:
- All use cases testable in isolation
- Domain logic has zero infrastructure dependencies
- In-memory adapters swap cleanly with production stubs
- No business logic in presentation layer
Feature Completeness:
- Driver can create profile
- Driver can join single league
- Admin can create league
- Schedule displays companion-scheduled races
- Results import updates standings
- Standings calculate correctly
User Experience Validation
Admin Workflow:
- Admin creates league in <2 minutes
- Admin imports results in <30 seconds
- Admin views standings with zero manual calculation
Driver Workflow:
- Driver creates profile in <1 minute
- Driver finds and joins league in <2 minutes
- Driver views schedule without asking admin
- Driver sees results immediately after import
Value Proposition Validation
Automation Integration:
- Companion schedules race → web app displays schedule (proves connectivity)
- Web app schedule guides companion automation (proves unified workflow)
Unified Management:
- Single source of truth for league data
- No Discord/spreadsheet required for standings
- Driver profile persists across sessions (in-memory during session)
Implementation Phases
Phase 1: Foundation (Week 1-2)
Deliverables:
- Mode system:
pre-launch→alphatransition - Middleware: mode-gated routes
- Domain models: Driver, League, Race, RaceResult, Standing
- In-memory repositories: all entities
- Basic UI layout: dashboard, navigation
Dependencies:
- Clean Architecture patterns from companion
- Next.js 15 app router
- TypeScript strict mode
Phase 2: Driver & League Core (Week 3-4)
Deliverables:
- Use case: CreateDriverProfile
- Use case: CreateLeague
- Use case: JoinLeague
- UI:
/profile/create - UI:
/leagues(browse) - UI:
/leagues/create - UI:
/leagues/:id(detail) - Unit tests: domain validation
- Integration tests: repository operations
Dependencies:
- Phase 1 complete
- Driver/League value objects
Phase 3: Scheduling & Companion Integration (Week 5-6)
Deliverables:
- Use case: ScheduleRace (from companion)
- IPC stub: companion → web app communication
- Domain model: Race status transitions
- UI:
/leagues/:id/schedule - UI: Race detail cards
- Integration tests: companion scheduling flow
Dependencies:
- Phase 2 complete
- Companion POC running
- HostedSessionConfig understanding
Phase 4: Results & Standings (Week 7-8)
Deliverables:
- Use case: ImportRaceResults
- Use case: CalculateStandings
- CSV parser: iRacing results format
- PointsPreset: F1 2024 preset
- UI:
/leagues/:id/races/:raceId/results/import - UI:
/leagues/:id/races/:raceId/results - UI:
/leagues/:id/standings - Unit tests: points calculation
- Integration tests: results → standings pipeline
Dependencies:
- Phase 3 complete
- iRacing CSV samples
Phase 5: Polish & Testing (Week 9-10)
Deliverables:
- E2E tests: full user flows
- Error handling: all use cases
- Loading states: all async operations
- Mobile responsive: all views
- Theme compliance: THEME.md
- Voice compliance: VOICE.md
- Documentation: alpha usage guide
Dependencies:
- Phase 4 complete
- Design system from landing page
Known Limitations (Alpha Scope)
Explicitly Deferred Features
Authentication & Authorization:
- No login/signup
- No user sessions
- No permission checks
- Rationale: In-memory data resets on restart; auth irrelevant for testing workflows
Data Persistence:
- No database
- No file storage
- Data clears on server restart
- Rationale: Alpha validates domain logic and UI flows, not persistence
Multi-League Support:
- Driver joins single league only
- No cross-league profile
- No multi-league standings
- Rationale: Simplifies alpha scope; multi-league in production
Advanced Scoring:
- No drop weeks
- No bonus points (fastest lap, etc.)
- Single points preset only
- Rationale: Complex scoring deferred to production
Team Competition:
- No team management
- No team standings
- No team profiles
- Rationale: Driver/admin flows first; teams in production
Result Automation:
- Manual CSV import only
- No iRacing API integration
- No live result fetch
- Rationale: Proves import pipeline; automation post-alpha
Complaint & Penalty System:
- Not implemented in alpha
- Rationale: Core competition flows first; penalties in production
Multi-Sim Support:
- iRacing only
- Rationale: Alpha validates single-sim workflows
Rating System:
- Not implemented in alpha
- Rationale: Requires production data volume
Alpha Success Definition
Alpha is successful if:
- Technical: In-memory adapters swap cleanly with production stubs (no domain/app changes)
- Functional: All 5 core features work end-to-end
- Integration: Companion schedules race, web app displays it
- Value: Admin workload reduced by automation visibility
- Architecture: Clean Architecture boundaries respected (testable via unit tests)
Alpha is ready for production when:
- Use cases remain unchanged
- Only infrastructure layer needs replacement
- UI components are production-ready
- Theme and voice guidelines followed
- E2E tests pass consistently
Post-Alpha Migration Path
Infrastructure Swap
Repositories:
// Alpha
const driverRepo = new InMemoryDriverRepository();
// Production
const driverRepo = new PostgresDriverRepository(pool);
Authentication:
// Alpha: No-op adapter
class NoAuthService implements IAuthService {
async getCurrentUser() { return mockDriver; }
}
// Production
class NextAuthService implements IAuthService {
async getCurrentUser() { return getServerSession(); }
}
API Integration:
// Alpha: IPC stub
class StubCompanionClient implements ICompanionClient {
async scheduleRace() { /* in-memory */ }
}
// Production
class HTTPCompanionClient implements ICompanionClient {
async scheduleRace() { return fetch('/api/races'); }
}
Zero Domain Changes
Driver,League,Raceentities unchangedCreateDriverProfileUseCaseunchangedCalculateStandingsUseCaseunchanged- UI components unchanged
Only wiring changes:
// apps/website/app/providers.tsx
const mode = getAppMode();
const repositories = mode === 'alpha'
? createInMemoryRepositories()
: createProductionRepositories(db);
Theme & Voice Compliance
Visual Guidelines (THEME.md)
Colors:
- Deep Graphite
#0E0F11(background) - Primary Blue
#198CFF(accents) - Performance Green
#6FE37A(success states)
Typography:
- Inter font family
- Semibold headings
- Light/regular body
Animations:
- 150-250ms transitions
- Subtle hover states
- Smooth scrolling
Components:
- 6-8px rounded cards
- Soft shadows (blur 20-28px)
- Hover glow on interactive elements
Voice Guidelines (VOICE.md)
Tone:
- Calm, clear, direct
- No corporate jargon
- No startup hype
- No gamer slang
Examples:
✅ "Profile created."
❌ "Awesome! Your profile is ready to rock!"
✅ "League joined."
❌ "Congratulations! You're now part of an amazing community!"
✅ "Results imported."
❌ "Success! We've synced your race data!"
Architectural Decisions
ADR-001: In-Memory Adapters for Alpha
Decision: Use in-memory repositories, not database, for alpha.
Rationale:
- Validates domain logic without infrastructure complexity
- Faster iteration on business rules
- Clean Architecture principles: domain independent of persistence
- Production migration only touches infrastructure layer
Trade-offs:
- Data lost on restart (acceptable for alpha testing)
- No concurrent user support (single-user alpha)
- Limited data volume testing (acceptable for workflows)
ADR-002: Mode System vs Feature Flags
Decision: Use environment-based mode system (GRIDPILOT_MODE=alpha), not feature flags.
Rationale:
- Clear separation: landing vs alpha vs production
- No flag management complexity
- Environment-appropriate defaults
- Aligns with existing
pre-launch/post-launchpattern
Trade-offs:
- Requires redeployment to switch modes (acceptable for alpha)
- No gradual rollout (not needed for internal alpha)
ADR-003: Manual Result Import for Alpha
Decision: Admin uploads CSV, no iRacing API integration in alpha.
Rationale:
- Validates result processing pipeline
- Avoids iRacing API rate limits during testing
- Simpler alpha scope
- CSV upload pattern works for production fallback
Trade-offs:
- Manual admin step (acceptable for alpha)
- No live result sync (deferred to production)
ADR-004: Single League Per Driver for Alpha
Decision: Drivers join one league maximum in alpha.
Rationale:
- Simplifies UI flows
- Reduces data model complexity
- Validates core competition workflow
- Multi-league requires persistent identity (production concern)
Trade-offs:
- Cannot test cross-league scenarios (acceptable for alpha)
- Multi-league profile in production
ADR-005: No Authentication in Alpha
Decision: No login, no sessions, no permissions.
Rationale:
- In-memory data has no persistent identity
- Auth complexity distracts from workflow testing
- Production auth requires database anyway
- Alpha tests business logic, not security
Trade-offs:
- Cannot test multi-user scenarios (acceptable for alpha)
- Auth added in production with database
Summary
Alpha proves: Automation + unified management reduces admin workload and consolidates fragmented tools.
Technical approach: Production domain/application logic with in-memory infrastructure.
Core features: Driver profiles, league management, race scheduling, result import, standings calculation.
Success criteria: Clean Architecture validated, workflows functional, companion integration working.
Next step: Production migration swaps only infrastructure adapters; domain and UI unchanged.
Timeline: 10 weeks to functional alpha demonstrating value proposition.