stint9 · system schematic

Backend logic — how the data actually moves

You built the front end. This is what sits behind it: a static site with no server of its own, a Supabase database it talks to directly, and — on race day — a live feed borrowed from stint9's own API.

Front end (browser) Supabase (database) stint9 live source Live race data Crew action
01

The shape of it

three zones, no middle server

The dashboard is a plain HTML page on GitHub Pages. It has no backend of its own — instead the browser talks straight to Supabase over REST, writing with a public key that row-level security keeps in check. The only external system is stint9's live-timing API, and only your logged-in browser can reach it.

Front end · GitHub Pages
index.html

The dashboard

Map, leaderboard, fuel, positions. Static, ~840 KB, no build step.

tyre.html · iframe

Tyre board

Self-contained sub-app, talks to the dashboard by postMessage.

Backend · Supabase
PostgREST + RLS

Tables

Timing, crew data, messages. Read by anyone; writes gated by policy.

Edge function

Scrapers (optional)

Per-dashboard update jobs; the live collector runs in-browser instead.

External · stint9
stint9.com/app

Live-timing API

/api/worker/… JSON behind Clerk login. The race feed's origin.

browser → writes → Supabase (public key + RLS) browser ← reads ← Supabase (polling) logged-in tab → reads → stint9 API (Clerk cookie)
02

Two data sources, one renderer

SIM / LIVE toggle

Every view draws from a single in-memory DB object. Where that object comes from is the only difference between the two modes — the map, positions and charts don't care which.

SIM · default

Baked-in CSV replay

A past NLS event, pre-processed offline into a const DB embedded in the page. The scrubber replays it. Nothing leaves the browser.

source
gen_db.py → embedded DB
persist
in-memory (practice)
LIVE · race day

Rebuilt from live rows

Polls Supabase for raw lap rows and rebuilds the exact same DB shape via build-db.js — verified to match the offline generator.

source
stint9_live_timing
persist
Supabase (per event-day)
03

The LIVE pipeline

stint9 → Supabase → you

stint9 already ingests the timing feed, so instead of scraping WIGE we borrow stint9's own JSON. A snippet in your logged-in tab bridges it into Supabase; the dashboard reads from there. Two independent loops, joined by one table.

01 · external

Logged-in stint9.com/app tab

Your Clerk session — the only thing allowed to read the feed. Serves /api/worker/events/{id}/laps as JSON.

02 · crew action

collector.js pasted in the console

Polls the laps endpoint every 5 s, maps each lap to a row, and upserts it.

rides the Clerk cookieauto-detects eventIdwrites with publishable key
03 · supabase

stint9_live_timing

Raw per-lap rows: car · lap · s1–s5 · time-of-day · pit. One row per (car, lap); the collector upserts, RLS allows the anon write.

04 · front end

Dashboard poll (every 5 s)

In LIVE mode the page fetches today's rows and hands them on. No stint9 credentials ever touch the dashboard.

reads with publishable key
05 · front end

build-db.js → buildClass → render

Rebuilds the DB (positions, legs, sector times), re-indexes the selected class, and repaints the map at the latest lap. Identical code path to SIM.

04

What Supabase stores

read = anyone · write = RLS-gated

All tables are public-read (the dashboard uses the anon publishable key) and write-gated by row-level-security policy — the same pattern across every table.

Live timing

stint9_live_timing

key · event_date, car, lap

Raw laps the collector writes; the dashboard rebuilds the DB from these.

collector ✎dashboard ↺

stint9_messages

key · id

Race-control board — penalties, flags. Scraper-fed; test rows as fallback.

scraper ✎board ↺
Per-car crew data — one board per car

stint9_fuel_state

key · event_date, car

Fuel entries + per-car calculator settings (tank, consumption) in state.set.

crew ✎fuel reel ↺

stint9_fuel_notes

key · event_date, car, lap

Free-text pit-wall notes, one per lap.

crew ✎fuel reel ↺

stint9_tyre_state

key · car

The whole tyre board as one JSON blob — stock, serials/km/cycles, moves, highlights.

tyre board ✎tyre board ↺
Telemetry & housekeeping

stint9_weather

key · id

Periodic track temp / conditions logged in LIVE for lap-time correlation.

dashboard ✎

stint9_laptimes

key · id

One row per completed lap with the temperature at the time.

dashboard ✎

stint9_site_status

key · id = 1

Single ONLINE/OFFLINE flag toggled from admin.html.

admin ✎page load ↺

stint9_reel_usage · _visit · _feedback

key · id

Usage analytics and the feedback widget.

dashboard ✎admin ↺
05

Two teams, one link, no collisions

keyed by car

Because fuel, notes and tyres are all keyed by the selected car, two teams sharing the link — each on a different car — write to different rows and never overwrite each other. No login, no localStorage; the database is the single source of truth, so it also syncs across devices.

Team A · car #650

fuel · notes · tyres

…_state (car=650)

separate
rows
Team B · car #667

fuel · notes · tyres

…_state (car=667)

Still shared on purpose: either team can select the other's car and see its data. Real team isolation (each side sees only its own cars) is the one planned feature left before a wide rollout.

06

The tyre board is its own app

iframe · postMessage

tyre.html runs in an iframe so its ~260 variables and styling can't collide with the dashboard's. They speak over a three-message channel.

dashboard → board

postMessage

tyreCar
selected car → load that car's blob
tyreLive
car progress → tick TRACK km
tyreH
sub-reel navigation
board → supabase

Per-car blob

On a car change it flushes the outgoing car, then loads & re-renders the incoming car's stint9_tyre_state row — or a fresh default board if that car has none.

debounced upsertno localStorage