Any third party can now list OpenRally tournaments, register players and receive live updates with one API key. TPS is the first consumer: a “From OpenRally” section in Competitions with naked portrait tiles, a detail screen, a three-step registration flow, and counts that move in real time. Built, security-reviewed twice, verified end to end on a local stack. Nothing is deployed.
14 s recording of the iPhone simulator. Full video: demo-live.mp4
What you are watching: the TPS app sits on the Play tab. From a terminal, a doubles team is registered into Bangkok Open through the public API with the TPS secret key. About a second later the tile reads 22 of 32 teams · 10 spots left where it read 21 and 11. Nobody touched the phone. The same path was proven from the OpenRally web app and from the TPS registration flow itself.
curl -X POST $API/tournaments/<uuid>/registrations \
-H "X-OpenRally-Key: ork_live_…" \
-H "X-OpenRally-Idempotency-Key: $(uuidgen)" \
-d '{"type":"doubles","category_id":"…",
"player":{"email":"carlos@…","display_name":"Carlos Demo"},
"partner":{"email":"dan@…","display_name":"Dan Demo"}}'
# → 201 Registration … and on the stream, ~1 s later:
id: 412
event: registration_created
data: {"type":"registration_created","data":{"tournament":{"capacity":
{"confirmed":22,"draw_size":32,"spots_left":10,…}},"registration":{…}}}
Everything else the app agent verified live against the real API: solo and doubles joins as Nestor’s own TPS identity, waitlist placement when a division fills, withdraw putting the spot back, the “You’re in” state surviving a restart, and missed events replayed over Last-Event-ID after the app was backgrounded.















One Edge Function router in OpenRally’s Supabase: /functions/v1/openrally-api/v1/*. OpenRally owns its API; no dependency on the Setpoint Rust protocol server or Fly.
ork_live_ secret keys for servers (all scopes) and ork_pub_ publishable keys safe to ship in an app (read + stream, per-client rate buckets via X-Client-Id). Only sha256 is stored.
Partners send email + display name. Existing OpenRally users are matched; otherwise a claimable placeholder is created, the same rule organisers already use. Players are or://openrally/player/{id}.
Triggers on tournament, registration, category, venue, draw, match and placement write protocol_event. A change made in OpenRally web fires exactly like one made through the API. Every event carries the full tournament snapshot with capacity.
Signed webhooks (X-OpenRally-Signature: v1=HMAC(ts.body), claim-based worker, backoff, SSRF guard) for servers, and an SSE stream with Last-Event-ID replay for clients.
Counts are in teams: confirmed = doubles + floor(solo/2), plus solo_players so the UI can say “13 solo players looking for a partner”. Doubles are two rows in OpenRally; is_primary marks the canonical one and lists return one entry per team.
Two adversarial passes with live probes. Round one: 3 critical8 warning6 info. Round two closed everything, then found an IPv6-mapped SSRF bypass and an uncommitted fix, both closed. Final verdict PASS.
| Finding | What it was | Fix |
|---|---|---|
| C1 | Any TPS user could cancel another user’s registration by planting a receipt row | Cancel requires player.email that is a member of the team; link ownership alone is never enough; TPS RLS refuses foreign registration ids |
| C2 | Deterministic idempotency key replayed a stale “confirmed” after withdraw; business 409s cached 7 days | Fresh UUID per attempt on TPS; API caches only 2xx and validation 4xx; 60 s in-flight takeover bounded by created_at |
| C3 | One rate-limit bucket for every app install | Per-client buckets (X-Client-Id / CF-Connecting-IP / last XFF hop) under a 2000/min key ceiling |
| W4 | Webhook SSRF: private ranges, redirects, body echo; then IPv4-mapped IPv6 literals | Resolve + blocklist incl. mapped/NAT64/6to4, redirect: error, per-delivery recheck, no body echo |
| W5–W10, I11–I17 | Worker lock that never held, out-of-order patches, unpublish/delete never emitted, unbounded inputs, cursor injection, error leakage, withdrawn history readable by app keys… | Claim RPC with SKIP LOCKED, per-row event guards, new tournament_unpublished/deleted events, caps, validated cursors, request ids, forbidden_status for publishable keys |
| Stream | Where | Highlights |
|---|---|---|
| Contract | openrally/apps/product-hub/Plans/openrally-open-api-v1.md | Nine decisions, objects, endpoints, DB, TPS integration, demo script, deploy dependencies |
| Data layer | apps/backend/supabase/migrations/20260902100000_open_api_v1.sql, scripts/{mint,revoke}-api-key.ts, scripts/seed-open-api-demo.ts | 7 tables, SQL serializer, emitter, 7 trigger sets, claim + rate-limit RPCs, generated posters, 20 tests |
| API | apps/backend/supabase/functions/openrally-api, …/openrally-api-webhook-worker, openapi/openrally-api.v1.yaml, docs/OPEN-API.md | 18 routes, auth/scope/rate/idempotency middleware, SSE, signed worker, OpenAPI, smoke script + echo receiver |
| Developer site | apps/developers/ (static, zeroship-ready) | Quickstart, reference, webhooks + signature samples in TS/Py/Go, SSE guide, TPS embed guide, live stream demo box |
| TPS backend | padel-backend-wt-openrally: migration + openrally-tournaments-{webhook,sync,join,cancel} | Signed webhook receiver → mirror + Realtime, prune-safe sync, join/cancel proxies holding the secret key, 42 tests |
| TPS app | mobile-app-padel/lib/features/openrally/ behind flag tps-0-openrally-tournaments | API client + SSE parser with reconnect, section, detail, 3-step registration reusing the partner picker, live patching, 90 tests |
hqcwmjninvunoexccrbz, not in the Supabase MCP org): apply 20260902100000_open_api_v1.sql, deploy openrally-api and openrally-api-webhook-worker, set API_WEBHOOK_WORKER_SECRET, the two GUCs and the worker URL, schedule the cron, mint the TPS secret + publishable keys (activation SQL in docs/OPEN-API.md).tnyqbaqnugjgefkdvmag): apply the mirror migration, deploy the four functions, set OPENRALLY_API_URL/KEY/WEBHOOK_SECRET, create the webhook subscription (curl in docs/openrally-tournaments.md).api.openrally.co in front of the function URL; publish apps/developers with zeroship.Local demo stays live on this machine: OpenRally stack on ports 563xx, API served, join/cancel proxies on 8801/8792, OpenRally web on :3005, app on the iPhone 16e simulator. Recipe in memory and in docs/OPEN-API.md.