A comprehensive Pinewood Derby race management system inspired by DerbyNet. Built with Bun, SQLite, and a bold racing aesthetic optimized for projection displays.
- Event Management: Create and manage race day events
- Racer Registration: Add racers with den/rank information
- Car Registration: Assign car numbers, names, and classes
- Inspection Tracking: Pass/fail inspection workflow
- Heat Generation: Automatic balanced lane rotation algorithm
- Live Race Console: Real-time heat management with big buttons
- Standings: Auto-calculated win/loss rankings with tie-breaking
- Projection Display: Full-screen display optimized for walls/projectors
- Win/Loss Priority: Records place (1st, 2nd, 3rd, 4th) rather than times
- Optional Times: Can capture times for tie-breaking but not required
- Balanced Lanes: Heat generation ensures even lane distribution
- Fast Race Day Flow: Minimal clicks, obvious actions, big buttons
- 50 Car Support: Designed for ~50 cars and ~100 people
- Bun (v1.2+)
- Modern web browser
# Install dependencies
bun install
# Run database migrations
bun run src/migrate.ts
# Start the server with hot reload
bun start
# Open http://localhost:3000bun test # unit + integration tests (requires server on :3000)
bun run test:unit # unit tests only — no server needed
bun run test:ui # Playwright e2e tests (optional locally, always runs in CI)Run a full simulated race flow (check-in -> heat generation -> partial run -> server restart -> finish) in an isolated SQLite file:
bun run rehearsal:race-dayUseful options:
# Keep the rehearsal DB for post-run inspection
bun run rehearsal:race-day --keep-db
# Customize scale/format
bun run rehearsal:race-day --cars 50 --lookahead 2 --pre-restart-heats 20- Navigate to the home page
- Click "Create Event"
- Enter event name, date, and lane count (typically 4)
- Click "Register" in the nav
- Add racers (first name, last name, den/rank optional)
- Add cars for each racer (car number, name, class)
- Run inspection and mark cars as passed
- Go to "Heats" page
- Click "Generate Heats"
- System creates balanced lane assignments automatically
- Go to "Race" page for the race console
- Project the "Display" view on a wall (open in new tab)
- For each heat:
- Click "START HEAT" to begin
- Cars race down the track
- Record finish order by clicking 1st, 2nd, 3rd, 4th or DNF
- Click "Complete Heat & Save" to record results
- System auto-advances to next heat
- Go to "Standings" page to see rankings
- Winners calculated by: Wins DESC, Losses ASC, Avg Time ASC
- Top 3 highlighted with gold/silver/bronze styling
GET /api/events- List all eventsPOST /api/events- Create eventGET /api/events/:id- Get event detailsPATCH /api/events/:id- Update eventDELETE /api/events/:id- Delete event
GET /api/events/:eventId/racers- List racers for eventPOST /api/events/:eventId/racers- Add racerGET /api/racers/:id- Get racer detailsPATCH /api/racers/:id- Update racerDELETE /api/racers/:id- Delete racer
GET /api/events/:eventId/cars- List cars for eventPOST /api/events/:eventId/cars- Add carGET /api/cars/:id- Get car detailsPATCH /api/cars/:id- Update carDELETE /api/cars/:id- Delete carPOST /api/cars/:id/inspect- Mark inspection pass/fail
GET /api/events/:eventId/heats- List heats with lanesPOST /api/events/:eventId/heats- Create heat manuallyDELETE /api/events/:eventId/heats- Clear all heatsGET /api/heats/:id- Get heat detailsPOST /api/heats/:id/start- Start a heatPOST /api/heats/:id/complete- Complete a heatPOST /api/events/:eventId/generate-heats- Auto-generate balanced heats
GET /api/heats/:heatId/results- Get results for heatPOST /api/heats/:heatId/results- Record results (batch)GET /api/events/:eventId/standings- Get race standingsGET /api/events/:eventId/standings/:className- Get class-specific standings
SQLite database with the following tables:
- events: Race day events with lane count and status
- racers: Scout racers with den/rank info
- cars: Car entries linked to racers
- heats: Race heats with round/heat number
- heat_lanes: Assignment of cars to lanes per heat
- results: Finish results with place and optional time
- standings: Materialized win/loss statistics
Migrations handled by Umzug.
- / - Event selector and creation
- /register - Racer and car registration with inspection
- /heats - Heat schedule preview and generation
- /race - Live race console for recording results
- /standings - Race results and rankings
- /display - Full-screen projection view (auto-rotates)
derby-timer/
├── src/
│ ├── index.ts # Bun server with routes
│ ├── migrate.ts # Database migration runner
│ ├── db/
│ │ ├── connection.ts # SQLite connection
│ │ ├── umzug.ts # Migration setup
│ │ ├── migrations/
│ │ │ └── 001_initial_schema.ts
│ │ └── models/
│ │ ├── events.ts
│ │ ├── racers.ts
│ │ ├── cars.ts
│ │ ├── heats.ts
│ │ └── results.ts
├── frontend.ts # SPA frontend
├── index.html # HTML entry
├── styles.css # Racing-themed styles
└── docs/
├── race-day-plan.md
└── ui-examples/
The system uses a balanced lane rotation algorithm:
- Fewer cars than lanes: Rotates starting positions so each car runs in each lane across rounds
- More cars than lanes: Uses round-robin rotation with lane shifting to distribute cars evenly
- Validation: Checks that each car hits every lane when mathematically possible
- Runtime: Bun (TypeScript, built-in bundler)
- Database: SQLite via
bun:sqlite - Migrations: Umzug
- Frontend: React SPA
- Styling: CSS with racing theme (Oswald + Space Grotesk fonts)
- Server: Bun.serve() with HMR
Built for fast-paced Pinewood Derby race days. 🏆