A space-traffic sandbox where the public site is the product. Scrolling falls from deep space onto a live globe of 18,050 catalogued objects, all propagated with SGP4 in the browser. The story then hands over to an operator’s workspace. There you can scrub 24 hours, fly into a satellite’s orbital plane, open a high-risk conjunction, watch the encounter with its uncertainty, plan two avoidance burns, and save an alert rule. Every burn is re-screened against the whole catalog before it is offered.
Built from blueprint 01 of the Motion-First Interactive Product Engineering Build Book (Volume V) as a vertical slice: the demo scenario is built end to end on the book’s stack. The parts that are not built are listed under Not built, and why.
Every object, operator, orbit, covariance and conjunction is generated by this repository. Nothing is a real satellite, a real catalog number or a real operator. The only external data are the SGP4 verification vectors published with Vallado et al. (AIAA 2006-6753), used as golden tests.
docs/video/demo.mp4docker compose up --build -d --wait # web :8431, API :8430, Postgres :55430, mail sink :8432, OTel collector, generator
make test # 72 unit + API contract tests in Docker against a throwaway Postgres (about 45 s)
make e2e # 7 Playwright journeys against the static export (needs Google Chrome)
make load-test # HTTP concurrency + 10,000 WebSocket subscriptions against a running API
make seed | make reset | make record # regenerate data / wipe and reseed / re-record the demo video
Open http://localhost:8431. This is the same site as the public sandbox, but built to call the API. Alerts you save there arrive
as e-mails at http://localhost:8432. The demo tokens are for local use only: aurora-operator-demo, aurora-viewer-demo and
lattice-operator-demo (a second operator, used to test tenant isolation). The static sandbox needs no backend:
npm ci && node data/simulators/generate.ts && npm run build -w @orbital/web && node scripts/serve.ts.
Two avoidance maneuvers. The planner searches 7 lead times × 2 directions, with bisection on the burn size, for the smallest in-track burn that brings Pc to 1 × 10⁻⁶. Then it re-screens every candidate’s new orbit. The cheapest burns were rejected: 2.84 cm/s four orbits early clears this conjunction but moves the 07:45 pass with the same rocket body to 134 m (Pc 3.0 × 10⁻⁴). 8 of the 10 candidates checked failed this way. The two offered options are:
| Option A | Option B | |
|---|---|---|
| Burn | 08:33 UTC, 0.5 orbits early | 08:57 UTC, 0.25 orbits early |
| Δv (retrograde) | 7.65 cm/s | 16.26 cm/s |
| Propellant (300 kg, Isp 220 s) | 10.6 g | 22.6 g |
| New miss / Pc | 707 m / 1.0 × 10⁻⁶ | 328 m / 1.0 × 10⁻⁶ |
| Worst new approach, rest of window | 3.40 km (Pc 1.4 × 10⁻¹⁴) | 612 m (Pc 7.1 × 10⁻¹⁷) |
alert_deliveries and the audit log.The URL tracks the state: for example ?cj=d9bb6a9c&mode=plan&opt=a&t=2026-10-06T09:20:00Z reopens the plan with option A
chosen.
flowchart LR
subgraph Browser
UI[Next.js + React UI<br/>store = source of truth] --> MO[packages/motion<br/>state machines + tweens]
MO --> R3F[React Three Fiber scene<br/>Earth, 18k points, picking]
UI --> WK[Web Worker<br/>WASM SGP4, planner, passes]
UI --> TEL[packages/telemetry<br/>Web Vitals, FPS, GPU]
end
UI -- static mode --> FX[(fixtures JSON)]
UI -- api mode, packages/sdk --> API
subgraph Docker
API[Fastify API<br/>OpenAPI 3.1, idempotency,<br/>rate limits, audit] --> PG[(Postgres 16<br/>row-level security)]
API --> MAIL[Mailpit SMTP sink]
API --> OTEL[OTel collector]
GEN[generator<br/>data/simulators] --> FX
end
WK & API --> DOM[packages/domain<br/>catalog, SGP4, screening, Pc,<br/>maneuvers, re-screen, alerts]
| Piece | What it does |
|---|---|
packages/domain |
All the orbital logic, shared by the browser worker and the API. Catalog: seeded generator with constellations, sun-synchronous shells, three fragmentation clouds, scattered debris, rocket bodies, navigation, geostationary and Molniya-like orbits. TLE lines are formatted to the column spec with checksums. Planted encounters: Newton iteration on a secondary’s TLE (mean anomaly, RAAN, mean motion) so that SGP4 puts it at a chosen offset from the primary at a chosen time. Screening: perigee/apogee shell filter, then a 60 s pass using straight-line relative motion to find the sample nearest each approach, then Brent’s method on the exact SGP4 distance. Pc: 2D encounter-plane integral (Gauss–Legendre on the hard-body disc) of the combined covariance; synthetic covariance grows with element age. Maneuvers: two-body + J2 RK4 propagation of the burned-minus-unburned offset added to the SGP4 path; grid over lead time × direction, bisection on size. Re-screen: the nominal orbit is screened once with a 60 km volume, then each candidate re-checks only those passes. It falls back to a full screen if the burn moves the orbit more than 54 km. |
packages/motion |
Deterministic state machines (transition, replay) and an orchestrator that tweens values on an explicit clock. Business state never lives here. Reduced motion sets every duration to 0, and pause freezes tweens. |
packages/telemetry |
LCP/INP/CLS (web-vitals), FPS and p95 frame time, dropped frames, long tasks, GPU context loss, scene-load and shader-compile time, renderer counters. Generates a W3C traceparent for each API call. In API mode it beacons samples to /v1/telemetry. |
apps/web |
Next.js 16 static export. Intro chapters scroll over the persistent canvas, and the sandbox panels share the same scene. The worker uses satellite.js’s WASM bulk SGP4 (JS fallback). Includes a command palette, keyboard shortcuts, deep links, list/table view, aria-live announcements, a failure lab (feed outage, GPU context loss, 50,000-object stress) and a telemetry HUD. |
apps/api |
One Fastify process implementing the blueprint’s contract. Every request gets a bearer-token tenant + role, a token-bucket rate limit with RateLimit-* headers, and structured {error:{code,message,traceId}} errors. Mutations take an Idempotency-Key (same key and body replays, different body returns 409). There is a hash-chained audit log, /metrics with p50/p95/p99 per route, OpenTelemetry HTTP and Postgres spans, and a WebSocket feed of live positions. |
packages/sdk |
Typed client: bearer token, traceparent, idempotency keys, cursor pagination, ApiError. |
| Postgres | Tables from section 5 (organizations, space_objects, tle_sets, ephemeris_points, conjunction_events, maneuvers), plus tenant_id / created_by / updated_at / deleted_at where rows belong to an operator. Alert rules, deliveries, idempotency keys, audit and client telemetry are additions. The API connects as orbital_app under row-level security; the owner runs migrations. |
apps/web/src/journey.ts) decides which view follows an event. The scene derives camera and layer targets from the view and
tweens toward them. No product decision waits on an animation callback.?rm=1): camera flights and chapter transitions become instant, encounter
playback is replaced by the closest-approach state, and all information stays. An e2e test runs the whole flow this way./ search, Space play, ← → scrub, [ ] conjunctions, L list, M reduced motion, P pause, Esc back,
Alt + arrows to turn the globe.| Test | Result |
|---|---|
SGP4 against the 33 AIAA 2006-6753 cases (java_sgp4_ver.out) |
all within 1 m and 3 mm/s. Case 33334, the paper’s deliberate bad-mean-motion case, is refused as it should be. The Fortran and Matlab files in the same archive differ from the Java and C++ lineage by 960 m on object 23599 (the documented Lyddane fix), so the Java file is the reference. |
| Screening precision/recall vs a brute-force reference (5 s steps, no shell filter, 8 primaries × 400+ objects) | 1.0 / 1.0 |
| Encounter-plane Pc vs 4,000,000-sample Monte Carlo, 4 geometries, plus the closed form for a centred circular Gaussian | within 4 standard errors / to 1e-9 |
| Planned options | meet Pc ≤ 1e-6; a 3% smaller burn does not; the fast re-screen gives the same options and rejections as the full re-screen |
| Property tests | Hermite interpolation (exact on straight lines, hits endpoints), unit round trips, RK4 energy drift < 0.2% over a day |
| Coverage (domain + motion) | 98% statements, 100% lines (threshold 85%) |
| API (14 tests, every body validated against the published OpenAPI 3.1) | pagination, bbox, ephemeris limits, authz matrix (no token / unknown / viewer / operator), tenant escape (other operator gets 404 on Aurora’s ids and prefixes; RLS blocks a WHERE-less query and a cross-tenant insert), idempotency replay and 409, audit chain verified, 503 feed outage, trace propagation, 429 with headers, WebSocket stream |
| Playwright (7 journeys) | the section 17 walk with deep-link restore; reduced motion; keyboard only; feed outage and recovery; GPU context loss and recovery; 50,000-object stress; visual baselines of three motion states (shell reveal, encounter at TCA, plan view) |
docs/performance-e2e.json). About 1.5 MB of JavaScript (uncompressed) and 3.8 MB of catalog JSON.docs/load-test.json): 32 clients for 10 s gave 5,911 req/s, p50 4.2 ms, p95 10.8 ms, p99 13 ms,
0 errors. 10,000 WebSocket subscriptions opened in 0.95 s and received 9,918 position messages a second (one each per second).traceparent; the API logs the trace id in every structured log line, returns it in error bodies,
and exports HTTP and Postgres spans to the collector (docker compose logs otel)..github/workflows/ci.yml runs audit, type-check, unit + coverage and Playwright.apps/web Next.js site (intro + sandbox), scene, worker, UI
apps/api Fastify API, migrations, seed
packages/domain orbital logic + tests packages/motion state machines, tweens + tests
packages/telemetry frontend telemetry packages/sdk typed API client
data/simulators generator data/fixtures generated data, provenance, SGP4 vectors
tests/e2e Playwright tests/performance load test
infra Dockerfiles, nginx (CSP), OTel collector docs OpenAPI, load and perf results, video