A Brawlhalla statistics and analytics platform built with Effect, featuring real-time player stats, rankings, and historical data tracking.
| Component | Technology |
|---|---|
| Package Manager | Bun (v1.3.4) |
| Monorepo | Turborepo |
| Backend | Bun + Effect + @effect/platform |
| Frontend | React 19 + Vite + TanStack Router + TailwindCSS |
| Database | PostgreSQL + Drizzle ORM |
| Cache | Redis (LRU eviction) |
| Observability | OpenTelemetry + Grafana Stack (Alloy, Loki, Tempo) |
| i18n | Lingui |
| Type Safety | TypeScript + Effect Schema |
dair/
├── apps/
│ ├── api/ # Backend API server
│ │ ├── src/
│ │ │ ├── routes/ # API route handlers
│ │ │ ├── services/ # Business logic & external integrations
│ │ │ │ ├── archive/ # Historical data storage
│ │ │ │ ├── brawlhalla-api/ # External Brawlhalla API client
│ │ │ │ ├── cache/ # Redis cache service
│ │ │ │ ├── db/ # Database client (Drizzle)
│ │ │ │ ├── fetcher/ # HTTP client with cache-first strategy
│ │ │ │ └── rate-limiter/ # API rate limiting
│ │ │ └── workers/ # Background crawlers
│ │ └── migrations/ # Drizzle migrations
│ ├── client/ # Frontend React app
│ │ ├── src/
│ │ │ ├── features/ # Feature modules (i18n, layout, search)
│ │ │ ├── routes/ # TanStack Router pages
│ │ │ └── shared/ # Shared components & utilities
│ │ └── public/ # Static assets
│ └── monitoring/ # Grafana observability stack config
│ ├── alloy/ # OpenTelemetry collector config
│ ├── grafana/ # Dashboards & datasources
│ ├── loki/ # Log aggregation config
│ └── tempo/ # Distributed tracing config
│
├── packages/
│ ├── api-contract/ # Shared API contracts (Effect HttpApi)
│ │ └── src/routes/ # Route schemas & types
│ ├── brawlhalla-api/ # Brawlhalla API types & helpers
│ │ ├── src/api/schema/ # API response schemas
│ │ ├── src/constants/ # Game data (legends, weapons, tiers)
│ │ └── src/helpers/ # Parsing & calculation utilities
│ ├── brawlhalla-replays/ # Replay file parser
│ ├── brawlhalla-servers/ # Server location data
│ ├── common/ # Shared utilities (math, date, string)
│ ├── db/ # Database schema (Drizzle)
│ │ └── src/schema/
│ │ ├── archive/ # Historical data tables
│ │ │ └── brawlhalla/ # Player, legend, weapon, ranked history
│ │ └── auth/ # Auth tables (users, sessions, oauth)
│ └── schema/ # Shared Effect Schema definitions
│
└── scripts/
├── compose.ts # Docker compose CLI helper
└── migration/ # Data migration scripts
The API implements a multi-tier caching and data collection system designed to minimize external API usage while providing fast responses.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client App │────►│ Public API │────►│ Cache (Redis) │
└─────────────────┘ └────────┬────────┘ └────────┬────────┘
│ │
│ Cache Miss │ Cache Hit
▼ │ (Stale-While-
┌─────────────────┐ │ Revalidate)
│ Rate Limiter │ │
│ (Frontend 30%) │ │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Brawlhalla API │◄─────────────┘
│ (External) │ Background
└────────┬────────┘ Revalidation
│
▼
┌─────────────────┐
│ PostgreSQL │
│ (Historical) │
└─────────────────┘
The API uses a stale-while-revalidate pattern:
- Cache Hit (Fresh): Return cached data immediately
- Cache Hit (Stale): Return cached data AND trigger background revalidation
- Cache Miss: Fetch from external API (rate limited), cache result
This ensures users never wait on rate limiting if any cached data exists.
The external Brawlhalla API has strict rate limits (10 req/sec, 2000 req/15min). Capacity is split:
| Consumer | Per Second | Per 15 Minutes | Purpose |
|---|---|---|---|
| Frontend | 10 (max) | 600 (~30%) | User requests (priority) |
| Workers | 7 (max) | 1400 (~70%) | Background data collection |
Two scheduled crawlers run alongside the API server:
- Crawls 1v1, 2v2, and rotating rankings for all regions
- Stores snapshots in dedicated ranked history tables
- Lightweight: only fetches ranking pages, not player details
- Powers "recently active players" and "ranked queue" endpoints
- Crawls rankings and fetches full player stats for each player
- Heavy operation: makes individual requests per player
- Populates player history, legend history, weapon history tables
- Powers historical tracking and global rankings
| Table | Purpose |
|---|---|
player_history |
Full player stats snapshots |
player_legend_history |
Per-legend stats over time |
player_weapon_history |
Per-weapon stats over time |
ranked_1v1_history |
1v1 leaderboard snapshots |
ranked_2v2_history |
2v2 leaderboard snapshots |
ranked_rotating_history |
Rotating queue snapshots |
player_aliases |
Player name history |
clan_history |
Clan/guild snapshots |
bun installCreate apps/api/.env:
# Required
API_URL=http://localhost:3000
DATABASE_URL=postgresql://dair:dair@localhost:5432/dair
DEFAULT_CLIENT_URL=http://localhost:3001
BRAWLHALLA_API_KEY=your_api_key_here
# OAuth (required for authentication)
OAUTH_SECRET=your_random_secret
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Optional
API_PORT=3000 # default: 3000
ALLOWED_ORIGINS=* # default: * (comma-separated)
REDIS_URL=redis://localhost:6379 # default: redis://localhost:6379
CACHE_PREFIX=api:cache # default: api:cache
OTLP_ENDPOINT=http://localhost:4318
SERVICE_NAME=api
SERVICE_VERSION=0.0.0bun devThis command:
- Starts Docker services (PostgreSQL, Redis, observability stack)
- Runs database migrations
- Starts the API server with hot reload
- Starts the client dev server
- Opens Drizzle Studio for database inspection
| Service | Port | URL | Description |
|---|---|---|---|
| API | 3000 | http://localhost:3000 | Backend REST API |
| Client | 3001 | http://localhost:3001 | Frontend dev server |
| Drizzle Studio | 4983 | https://local.drizzle.studio | Database GUI |
| PostgreSQL | 5432 | - | Database |
| Redis | 6379 | - | Cache |
| Grafana | 3002 | http://localhost:3002 | Observability UI |
| Alloy | 12345 | http://localhost:12345 | OTEL collector UI |
# Start everything (services + dev servers)
bun dev
# Docker services
bun compose up # Start Docker services only
bun compose down # Stop Docker services
# Production
bun server:start # Start API in production mode
bun build # Build all packages
# Code quality
bun lint # Format + lint + fix
bun check:types # Type checking
bun check:lint # Lint only
bun check:format # Format check
bun check:deadcode # Find unused exports
bun test # Run tests
# Database (run from apps/api)
bun db:migrate # Generate + apply migrations
bun studio # Open Drizzle Studio
# Localization (run from apps/client)
bun locales:extract # Extract strings from code
bun locales:compile # Compile translation files| Service | Port | Description |
|---|---|---|
| Grafana | 3002 | Visualization and dashboards |
| Grafana Alloy | 12345 (UI), 4317 (gRPC), 4318 (HTTP) | OpenTelemetry collector |
| Grafana Loki | 3100 | Log aggregation |
| Grafana Tempo | 3200 | Distributed tracing |
- URL: http://localhost:3002
- Username:
admin - Password:
correcthorsebatterystaple
apps/monitoring/alloy/config.alloy- OTEL receiver configurationapps/monitoring/loki/loki-config.yaml- Log storage configurationapps/monitoring/tempo/tempo-config.yaml- Trace storage configurationapps/monitoring/grafana/provisioning/- Datasource & dashboard provisioning
This project follows idiomatic Effect patterns. For guidance:
bunx effect-solutions list # List all topics
bunx effect-solutions show <slug> # Read a specific topicKey patterns used:
- Services: Effect.Service with
@app/ServiceNametags andstatic readonly layer - Errors:
Schema.TaggedErrorwith all properties in the schema - Config: Dedicated config services per domain using Effect Config