FRC Scouting App
Real‑time match collection. Offline‑first reliability. Instant analytics that actually drive pick lists.
- Teams
- 80+
- Matches Logged
- 1,500+
- Offline Sessions
- 500+
- Median Sync
- <2s

Overview
Offline‑first PWA for field data collection. Deterministic sync. On‑device analytics for pick lists and drive‑team briefs.
Stack: React + TypeScript + Tailwind, FastAPI, SQLite/Postgres, WebSocket + HTTP, IndexedDB (Dexie).
Match input
± tap‑zones for atomic ops per metric (Auto/Teleop/End). Writes to local op‑queue (IndexedDB). Optimistic UI. Batch push on sync.
- Own demo sim: IndexedDB + mock HTTP batch endpoint.
- Idempotent keys: (event, match, team, phase, metric).
- Accessibility: large hit targets; no-scroll, full-screen.
Team analytics
Using various algorithms to analyze data, give strategy suggestions, and alliance simulation.
- AI: Using KMeans and random forest regressor to classify and predict match outcome.
- Heuristic: Using Theil-Sen estimator and score calculators to predict match scores.
- Elo: Using Bayesian inference and featured Elo to classify and rank robot.
Sync architecture
Uses HTTPS polling for live sync, DexieDB(IndexedDB) for offline scouting.
- HTTPS Polling: more stable than websocket at worse internet.
- live update: scouting data is uploaded live so admins get real time scouting data and basic analytics.
- DexieDB & PWA: allows scouting with no internet, and data syncs on reconnect.
Architecture
Client: React/TS, Tailwind, PWA. Storage: IndexedDB (Dexie). Transport: HTTP + WebSocket. Server: FastAPI, SQLite (dev) / Postgres (prod), Alembic migrations.
Storage
Client op‑queue + cache in IndexedDB; server tables keyed on (event, match, team, phase, metric) for idempotence.
Sync
Batch push with ULIDs; delta feed by cursor; retry with exponential backoff and jitter; live updates via WS.
Auth
JWT session cookies; role‑based access; kiosk API keys; edge allowlist + rate limiting.
Data model
Minimal event‑sourced ops schema. Derived metrics computed server‑side; clients render aggregated series.
export type Phase = "pre" | "auto" | "teleop" | "end";
export interface MatchEvent {
id: string; // ulid
eventKey: string; // e.g. 2025marea
matchKey: string; // qm12, qf2m1, etc.
team: number; // 0000–9999
phase: Phase;
metric: string; // e.g. coral.L3, algae.removed
delta: number; // +1/-1 atomic op
ts: number; // ms epoch
}
CREATE TABLE ops
(
ulid TEXT PRIMARY KEY,
event_key TEXT NOT NULL,
match_key TEXT NOT NULL,
team INTEGER NOT NULL,
phase TEXT NOT NULL,
metric TEXT NOT NULL,
delta INTEGER NOT NULL,
ts BIGINT NOT NULL
);
CREATE INDEX ops_event_cursor ON ops (event_key, ts);
API
Method | Path | Description |
---|---|---|
POST | /ops/batch | Push queued ops (idempotent) |
GET | /ops/delta?since=ULID | Stream new ops since cursor |
GET | /teams/:team/summary | Computed metrics per team |
GET | /event/:key/export.csv | CSV export (offline review) |