Architecture
Monorepo & pipeline map
Layout
f1datastop/
├── web/ # Vite React 18 + TS app (port 8080, builds to web/dist, Vercel deploys it)
├── supabase/ # functions/ (f1-proxy, bmc-webhook) + migrations/ (fresh-DB squash + forward)
├── analysis/ # Python: ingest, backfill, generate, *_export, queue processors
├── api/og.ts # Vercel OG-image function (bot user-agent rewrite)
├── docs-site/ # THIS site — plain static HTML served by GitHub Pages
└── .github/workflows/ # pr-ci.yml (CI + cron), release.yml (manual), docs.yml (this site)
Data flow
Jolpica/Ergast ──┐
OpenF1 ──────────┼──▶ f1-proxy edge fn (+ api_cache) ──▶ web (React Query hooks)
FastF1 (Python) ─┘ │ ▲
▼ │ public-read RLS
Supabase warehouse (f1_* tables/views) ───────────┘
▲
analysis/*.py (Actions cron) ──▶ Cloudflare R2 (cdn.f1datastop.com) ──▶ web Deep Dive tab
Serving rule: durable warehouse domains (results, laps, schedule) are DB-first behind the VITE_DB_SERVING_ENABLED flag (default off, rollback = flip flag). Volatile data (live timing, standings) stays on the proxy. Any DB miss falls through to proxy — a DB problem degrades to today's behavior, never a user-facing failure. Full spec: docs/DATA_BACKEND.md.
Session codes
Every layer uses the same codes: FP1 FP2 FP3 Q SQ SS S R (SQ = sprint qualifying/shootout, SS = sprint shootout legacy, S = sprint). Stored in f1_session.session_type, used by R2 paths and export scripts.
CI jobs (pr-ci.yml)
| Job | When | What |
|---|---|---|
web | every push + PR | typecheck, advisory lint, test, build |
python-smoke | every push + PR | pip install, byte-compile analysis/ |
reconcile | every 6 h + 30-min race-weekend ticks | ingest results, export season JSON to R2 |
charts | every 2 h Fri–Sun | render Deep Dive charts for finished sessions |
assets | 06/12/18 UTC Fri–Sun | telemetry, circuits, replays, weather |
digest | daily 07:17 UTC | free-tier usage digest to Slack |
queue | every 5 min | drain f1_chart_requests + f1_telemetry_requests |
All scheduled jobs are guarded to the main repo (github.repository == 'ibrahimroshdy/f1datastop'), so forks get zero-secret CI. Any job runs on demand via workflow_dispatch.