Imported from WolfeLeo2/Sideline (
AGENTS.md). Install upstream withnpx skills add WolfeLeo2/Sideline. Copyright stays with the author.
AGENTS.md — Sideline
This file is the operational reference for all AI coding agents working on this codebase. Read this before writing any code. For full details, see
docs/PRD.md.
Project Overview
Sideline is a personalized real-time sports companion app built with Flutter. It aggregates live scores, news, stats, and fantasy data — filtered through the user's personal following list.
Core value prop: One app replacing ESPN + Twitter/X + a fantasy app, showing only what the user cares about.
Tech Stack
Client (Flutter)
| Layer | Technology |
|---|---|
| Framework | Flutter (latest stable) |
| Language | Dart |
| State Management | Riverpod (with riverpod_annotation code generation) |
| Local Database | ObjectBox |
| Navigation | go_router |
| HTTP Client | Dio |
| Real-time | Supabase Realtime (MVP) |
Backend
| Layer | Technology |
|---|---|
| Runtime | Node.js LTS |
| Framework | Fastify (TypeScript) |
| Job Queue | BullMQ |
| Cache | Redis (Upstash for MVP) |
Infrastructure
| Service | Purpose |
|---|---|
| Supabase | Postgres DB, Auth, Realtime, Storage |
| Redis (Upstash) | Caching, job queues, rate limiting |
| Firebase | Push notifications (FCM) |
| Railway | Backend hosting (MVP) |
Project Structure
lib/
├── main.dart
├── app.dart
├── core/
│ ├── constants/ # colors.dart, typography.dart, spacing.dart
│ ├── theme/ # app_theme.dart
│ ├── router/ # app_router.dart
│ └── services/ # supabase_service.dart, notification_service.dart
├── features/
│ ├── auth/
│ ├── following/
│ ├── feed/
│ ├── live_game/
│ ├── scores/
│ ├── fantasy/
│ └── profile/
└── shared/
├── utils/ # time_utils, sport_icons, sport_utils
└── widgets/ # live_badge, section_header, app_error_state, app_empty_state, team_logo, app_bar_brand_icon, selectable_chip
Each feature folder structure:
features/<name>/
├── data/ # Repository implementations, API calls, ObjectBox queries
├── domain/ # Models, abstract repository interfaces
└── presentation/ # Screens, widgets, Riverpod providers
Architecture Rules
- Flutter NEVER calls third-party APIs directly. All sports data, news, and fantasy requests go through the Sideline backend.
- No API keys in Flutter code. All third-party keys live in backend environment variables only.
- Supabase RLS must be enabled on all user-scoped tables.
- Fantasy OAuth tokens stored encrypted via pgcrypto. Never plaintext.
- Rate limiting on all backend routes: 100 req/min per user via Redis.
- HTTPS and WSS only in production.
- DRY: Use shared widgets/utils. Before writing a new widget, check
lib/shared/widgets/andlib/shared/utils/. Never duplicate LIVE badges, section headers, error/empty states, team logos, or utility functions. - Public backend routes (no auth):
/health,/api/games,/api/stats,/api/trending,/api/teams/colors,/api/search,/api/h2h,/api/players,/api/teams/:sport. All other routes require Supabase JWT. - Reference is fair game. Code examples, patterns, design references, and assets found online are free to use, adapt, and integrate. If it's publicly available, treat it as a resource.
Design Paradigms
Apply the right principle for the situation — don't default to one:
| Paradigm | Apply When | Avoid When |
|---|---|---|
| DRY (Don't Repeat Yourself) | Same logic appears 3+ times with identical intent. Extract to shared widget/util. | Two copies serve different concerns or will likely diverge — forced DRY here creates brittle coupling. |
| KISS (Keep It Simple, Stupid) | Default stance. Prefer straightforward, readable code over clever abstractions. | You've confirmed a pattern repeats and benefits from abstraction — then abstract. |
| YAGNI (You Aren't Gonna Need It) | Tempted to build a "generic" solution for a single use case. Build only what's needed now. | You have concrete, immediate requirements for the generalization. |
| SOLID | Service/repository/provider layer boundaries. Single Responsibility, Interface Segregation, Dependency Inversion. | UI widgets — over-abstracting a widget into 5 classes is worse than one clear widget. |
| WET (Write Everything Twice) | Two copies exist but serve different concerns or will likely diverge (e.g., two screens with similar-looking cards but different data needs). Accept the duplication. | Third copy appears — now extract per DRY. |
| AHA (Avoid Hasty Abstractions) | You see 2 similar things and want to abstract. Wait. Abstract only after 3+ real cases confirm the pattern. | Pattern is crystal clear and stable (e.g., LIVE badge used identically in 4 places). |
Decision flow: KISS first → wait for repetition → AHA (wait for 3rd case) → DRY extract → keep SOLID at boundaries → YAGNI everything else.
Design System
Font
Google Sans Flex — bundled variable font for Material 3 Expressive look. Loaded as a bundled asset (not via google_fonts package). Available on both Android and iOS.
| Role | Weight |
|---|---|
| Display / Hero Scores | 800 |
| Headings | 700 |
| Labels / Captions | 500 |
| Body | 400 |
Colors (Dark-first)
| Role | Hex |
|---|---|
| Background | #0A0E1A |
| Surface | #141824 |
| Surface Elevated | #1E2333 |
| Accent Red (CTAs, live) | #E63946 |
| Accent Blue (data, links) | #4CC9F0 |
| Success Green | #2ECC71 |
| Warning Amber | #F4A261 |
| Text Primary | #F0F4FF |
| Text Secondary | #8892A4 |
| Text Muted | #4A5568 |
| Divider | #2A3044 |
Styling
Reference my app styling from /constants/ folder before making inline changes to a specific file. Only make inline specific styles (ie there is AppSpacing.xs which is 4 pixels from my spacing.dart. By inline styling i mean not using the AppSpacing.(value) and using your own spacing ie Radius.circle(2))
Avoid hardcoded fontSizes and prefer usting textTheme ie Theme.of(context).textTheme.display/heading/label....?.copyWith().
Navigation
5-tab bottom nav bar: Home | Live | Scores | Fantasy | Profile No hamburger menu or side drawer.
Coding Conventions
Dart / Flutter
freezedfor all model classes (immutable, copyWith, equality)json_serializablefor JSON parsing — no manualfromJson- All async operations use
AsyncValuefrom Riverpod — handle loading, data, and error in UI - Feature folders are self-contained with
data/,domain/,presentation/ - Shared widgets in
lib/shared/widgets/— never duplicate across features - File naming:
snake_case.dart - Class naming:
PascalCase - Never use
BuildContextacross async gaps ListView.builderonly — neverListViewwith children array- Loading states: shimmer skeleton loaders only — never
CircularProgressIndicator - Never show empty screens — always shimmer, error state, or empty state illustration
- Remote images:
cached_network_imagewith team color gradient placeholder - Complex list items: wrap in
RepaintBoundary - Cards:
BorderRadius.circular(10), backgroundAppColors.surface - Primary buttons:
BorderRadius.circular(12), backgroundAppColors.accentRed - All touch targets: minimum 48×48 logical pixels
TypeScript / Backend
- Fastify routes typed with zod schema validation on request and response
- DB queries use Supabase TypeScript client with generated types
- BullMQ jobs must be idempotent
- Validate all environment variables on startup using zod — crash fast if missing
- All errors logged with context — no silent failures
Git
- Branch naming:
feature/,fix/,chore/ - One feature per PR
MVP Scope (v1.0) — Build Only This
- Auth (email/password + Google via Supabase Auth)
- Onboarding (6 screens)
- Following system (teams, players, leagues — optimistic UI)
- Personalized news feed (20/page, infinite scroll, breaking news banner)
- Live game screen (play-by-play, box score, stats, win probability chart)
- Player & team profiles
- Scores tab (today's games, filterable by sport)
- Push notifications (Tier 1 breaking + Tier 2 live game)
- Offline caching via ObjectBox
- Backend: Fastify + Supabase + Redis + BullMQ
NOT in MVP
- Fantasy integration (v1.5)
- Unified sports calendar (v1.5)
- Fan Pulse / Rivalry Mode (v2.0)
- AI summaries (v3.0)
Key References
| Document | Purpose |
|---|---|
docs/PRD.md |
Full requirements, DB schema, API details, RLS policies |
docs/APIs.md |
ESPN API, NewsAPI, backend routes, Redis/Supabase roles, API justification |
docs/PROGRESS.md |
Living development progress tracker — update after completing work |
docs/01_feature_set.md |
Detailed feature descriptions and UX flows |
docs/02_technical_considerations.md |
API choices, backend architecture, infra, packages |
docs/03_ui_considerations.md |
Design language, screen designs, motion, accessibility |
Shared Widget & Utility Inventory
Before creating a new widget, check these shared components:
Utilities (lib/shared/utils/)
| File | Exports | Used By |
|---|---|---|
time_utils.dart |
timeAgo(DateTime), formatTime(DateTime) |
article_card, trending_news_card, article_detail_screen |
sport_icons.dart |
sportIcon(String sport) → IconData |
entity_card, scores_screen, following_screen |
sport_utils.dart |
sportEmoji(String), sportLabel(String) |
article_card |
Widgets (lib/shared/widgets/)
| File | Widget | Props |
|---|---|---|
live_badge.dart |
LiveBadge |
animated, variant (tinted/solid) |
section_header.dart |
SectionHeader |
title, leadingIcon, count, trailing, trailingIcon, onTrailingTap, padding |
app_error_state.dart |
AppErrorState |
icon, title, subtitle, onRetry, compact |
app_empty_state.dart |
AppEmptyState |
icon, title, subtitle |
team_logo.dart |
TeamLogo |
url, size, color, shape, borderRadius |
app_bar_brand_icon.dart |
AppBarBrandIcon |
icon |
selectable_chip.dart |
SelectableChip |
label, selected, onTap |