Space situational awareness, legible.
sat.trackr.live is a public, read-only, mobile-friendly 3D web application that visualizes everything humans have put into Earth orbit — satellites, debris, launches, reentries, conjunctions, and the space-weather environment shaping it all — on a single time-scrubbable globe with a full text-only fallback at /text.
Part of the trackr.live family alongside trackr.live and cyber.trackr.live.
- Executive summary
- Feature matrix
- Architecture overview
- Technology stack
- Installation
- Quick start
- Configuration
- Usage examples
- API documentation
- Security considerations
- Deployment
- Operational requirements
- Logging, monitoring, and telemetry
- Testing
- Troubleshooting and FAQ
- Contribution guidelines
- Versioning and release policy
- Licensing and legal
- Roadmap and project status
The chunk-by-chunk historical record of how the project got here lives at docs/phase_summary.md — useful for understanding why a feature exists, but the README below is the canonical operator-facing reference.
Orbital data is real, public, and chaotic. Space-Track ships TIP decay messages over a cookie-jar session. CelesTrak publishes TLEs as 38 newline-delimited group files. The Launch Library catalogs upcoming launches in a different shape entirely. SOCRATES dumps 145 K conjunction predictions as CSV. NOAA SWPC streams aurora-probability rasters every 15 minutes. Anyone wanting a unified, time-scrubbable, mobile-friendly view of "what's overhead right now" has to stitch all of these together themselves.
sat.trackr.live is the stitched-together view. One globe shows ~15,700 satellites color-coded by type, with Cesium handling the WebGL and satellite.js running SGP4 in a Web Worker so the propagation budget stays off the main thread. A right-rail panel surfaces the catalog metadata; a 📍 observer pill in the topbar grants the user a personal pass-prediction view. A dedicated /conjunction/{primary}/{secondary} replay route flies the camera alongside two on-orbit objects through their time of closest approach. The ↑ Sky view topbar toggle reframes the camera as a zenith-aimed planetarium pose for a stargazer use case. Every server-rendered surface has a text-only fallback at /text that works without JavaScript, without WebGL, and on a phone with three bars of signal.
- Amateur radio operators looking up SatNOGS-tracked transmitters
- Satellite hobbyists watching for ISS pass overheads
- Journalists writing about Starlink launches or rocket-body reentries
- Students taking an orbital-mechanics course
- Anyone who has ever asked "what's that bright moving dot in the sky right now?"
- Server-rendered HTML first, progressive enhancement to a 3D globe. Every meaningful surface has a
/text/*mirror that works without JavaScript. The SPA is the rich path, not the only path. - SQLite for everything. No external database server, no Redis, no message broker. The project is deliberately deployable on a $5/month VPS.
- Pure cron for data refresh. Every ingester is a CLI command (
make ingest,make ingest-spacetrack, etc.) that idempotently UPSERTs into the catalog. Cron schedules them; nothing pushes or queues. - No user accounts, no telemetry by default. The observer location is stored in the browser's
localStorage, never sent to the server. Analytics (Plausible) is opt-in via an env var. - Honest static analysis. PHPStan level 6, PHPUnit at 411 tests, Vitest at 361, Playwright at 146.
make ciis the local gate;make security-auditis the supply-chain gate.
| Versus … | What sat.trackr.live does differently |
|---|---|
| n2yo.com | Open-source AGPL; full text-only fallback; conjunction-replay flythroughs; runs entirely from your own VPS without n2yo's API quota |
| heavens-above.com | Modern 3D globe with WebGL terminator + starfield + city lights overlay; mobile-first responsive design; PWA-installable |
| Celestrak's home page | Searchable + linkable + sharable per-satellite pages; observer-aware pass predictions; ICS calendar export for upcoming passes |
| Roll-your-own scripts | Stitched and verified across CelesTrak / Space-Track / LL2 / SOCRATES / NOAA SWPC / SatNOGS so you don't have to |
Legend: ✅ stable · 🧪 experimental · 📋 planned ·
| Feature | Status | Notes |
|---|---|---|
| CelesTrak GP TLE ingest (38 groups) | ✅ | make ingest, ~15,700 satellites in ~40 s, honors HTTP 304 |
| CelesTrak SATCAT enrichment | ✅ | operator/country/launch_date/RCS/status, ~98.5 % coverage |
| Launch Library 2 (upcoming + recent launches) | ✅ | Free tier 15 req/hr; paid LL2_API_TOKEN removes the limit |
| Space-Track TIP decay messages | ✅ | Requires SPACE_TRACK_USER + SPACE_TRACK_PASS |
| SOCRATES conjunction predictions | ✅ | ~145 K close-approach rows; tri-color risk badge |
| NOAA SWPC space-weather snapshots | ✅ | Kp index + X-ray flux every 5 min |
| NOAA OVATION aurora-forecast raster | ✅ | 720×360 RGBA PNG, refreshed every 15 min |
| SatNOGS amateur-radio transmitters | ✅ | ~10K transmitter records, weekly refresh |
| SatNOGS amateur ground stations | ✅ | ~2,000 violet 3 px dots, qra_active 90-day window |
| Alpha-5 NORAD encoding | ✅ | Forward-compatible with the 6-digit NORAD ID rollover |
| Push-based ingest webhook | ✅ | POST /webhooks/ingest/{source} bearer-auth, default off |
| Feature | Status | Notes |
|---|---|---|
| ~15K satellites as Cesium PointPrimitives | ✅ | Color-coded by object type |
| SGP4 propagation in Web Worker | ✅ | 4 Hz tick, off the main thread |
| Time scrubbing (±7 d slider + 5 speeds) | ✅ | Yellow bands beyond ±48 h |
| Cesium lighting + day/night terminator | ✅ | |
| BSC5 starfield cubemap (~9,100 stars) | ✅ | make build-skybox regenerates |
| Selected-object orbit ribbons (½/1/2/3 orbits) | ✅ | Fading gradient |
| Marquee 3D model layer (ISS + others) | ✅ | LOD swap-in for self-hosted glTFs via make fetch-models |
| Ground-station catalog (41 curated) | ✅ | NEN/DSN/ESTRACK/JAXA/ISRO/KSAT/AWS/ATLAS |
| Sensor cones (5° half-angle) | ✅ | Per ground station |
| VIIRS night-lights overlay | ✅ | Dark-side only, lazy-loaded |
| Aurora-forecast raster overlay | ✅ | |
| GNSS constellation color overlays | ✅ | GPS/Galileo/GLONASS/BeiDou, default off |
| Sun-synchronous ground-track ribbons | ✅ | Landsat 8/9 + Sentinel-1/2/3/5P |
| Decay-event historical timelapse traces | ✅ | Last 30 days of TIP-message reentries |
| WebGPU instanced rendering | 📋 | Phase 12+ candidate when catalog grows past ~50K |
| WebGL required | /text fallback for non-WebGL devices |
| Feature | Status | Notes |
|---|---|---|
| Observer-pill (geolocation / city / manual) | ✅ | localStorage-persisted, never sent to server |
| Pass predictions (next 5 from observer) | ✅ | Server-cached 6 h; observer rounded to 3 dp |
| ICS calendar export | ✅ | GET /api/v1/satellites/{norad}/passes.ics?lat&lon&alt&days |
| Pass-prediction visual magnitude | ✅ | N2YO-enriched when N2YO_API_KEY present |
| Notification scheduling (in-page + SW) | ✅ | Per-pass 🔔; survives tab close via service worker + IndexedDB |
| Sky view (zenith-aimed planetarium pose) | ✅ | Topbar toggle gated on observer; ?view=sky deep-link |
| Bright-pass discovery in sky view | ✅ | RCS-estimated, top 30 candidates, mag ≤ 4.5 |
| Feature | Status | Notes |
|---|---|---|
| Right-rail detail panel | ✅ | Identity / current state / orbital elements / raw data |
| Mobile bottom-sheet detail panel | ✅ | ≤ 700 px viewport, 3 snap points + swipe-dismiss |
| Search with ⌘K + autocomplete | ✅ | FTS5-backed |
| Theme switcher (dark / light / high-contrast) | ✅ | prefers-color-scheme aware |
prefers-reduced-motion respect |
✅ | Across selection pulse / camera flyTo / sky-view entry |
| Settings ⚙ menu (lead time, opt-out, RM) | ✅ | Cookieless |
Share button + ?sat=… deep-links |
✅ | navigator.share with clipboard fallback |
| Per-pass 🔔 notify button | ✅ | Lead time 2/5/10/15 min |
| Feature | Status | Notes |
|---|---|---|
| Catalog list + pagination | ✅ | |
| Per-satellite detail page | ✅ | /text/satellite/{norad} |
/text/launches + /text/decays |
✅ | Countdowns, tri-color risk badge |
/text/conjunctions |
✅ | Sortable, paginated |
/text/space-weather + /text/stats |
✅ | ASCII-bar breakdowns |
/text/events + /events.atom |
✅ | Atom 1.0 syndication |
| Sortable column headers (server-side + JS hijack) | ✅ | 0.92 KB-gz progressive-enhancement bundle |
| Geolocation prompt with prog-enhancement fill | ✅ | [data-geolocation] |
| Feature | Status | Notes |
|---|---|---|
/conjunction/{p}/{s} dedicated route |
✅ | Chase camera + HUD + TCA pulse |
Replay from /text/conjunctions row |
✅ | Atom-feed entries also deep-link |
| Edge-case visualisations | ✅ | Co-orbital, near-pass, retrograde |
| Feature | Status | Notes |
|---|---|---|
| PWA manifest + icons | ✅ | Installable on mobile |
| Service worker cache-first build assets | ✅ | Vite content-hashed → safe forever within a version |
Service worker offline /text fallback |
✅ | Cached /offline.html |
| Notification SW (chunk 8 5D) | ✅ | IndexedDB-backed schedule; 60 s heartbeat |
| Feature | Status | Notes |
|---|---|---|
/stats operator/country/type/year breakdowns |
✅ | Pure GROUP BY over the existing catalog |
| Atom event-stream syndication | ✅ | Launches + reentries + significant conjunctions + storms |
| Feature | Status | Notes |
|---|---|---|
Server-side /state-now propagation cache |
✅ | 60 s APCu/SQLite cache + optional observer look-angles |
| TLE ingest reject-rate alerting | ✅ | Slack-compatible webhook; 30-min cooldown |
make security-audit gate |
✅ | composer audit + npm audit + Dependabot/CodeQL/secret-scanning |
make security-audit-dump verbose summary |
✅ | Always exits 0; for human review |
| OpenAPI 3.1 spec + Swagger UI | ✅ | /api/v1/openapi.json, /api/v1/docs |
| OG image generation | ✅ | /og/satellite/{norad}.png, etc |
| Sitemap (chunked) | ✅ | make sitemap-build |
| Feature | Status | Notes |
|---|---|---|
| Plausible (cookieless) | ✅ | Optional; env var + DNT + localStorage opt-out |
| Built-in user analytics | 📋 | Not planned — privacy-by-default |
┌────────────────────────────────────────────────────────────┐
│ Browser / mobile / phone │
│ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ SPA (Lit + Cesium)│ │ /text fallback (SSR) │ │
│ │ - SGP4 in Worker │ │ - server-rendered │ │
│ │ - Cesium WebGL │ │ HTML, no JS needed │ │
│ └────────┬─────────┘ └──────────┬─────────────┘ │
│ │ JSON API │ HTML │
└────────────┼──────────────────────────────┼─────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐
│ Slim 4 PHP front controller │
│ (public/index.php) │
│ │
│ ┌───────────────┐ ┌────────────────────┐ │
│ │ /api/v1/* │ │ /text/* server- │ │
│ │ JSON │ │ rendered controllers│ │
│ └───────┬───────┘ └──────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ PHP-DI 7 service container │ │
│ │ - StateNowService │ │
│ │ - PassCalculator + PassCache │ │
│ │ - SatelliteRepository, etc. │ │
│ └──────────────────┬───────────────────┘ │
└──────────────────────┼────────────────────────┘
│
▼
┌─────────────────────┐
│ SQLite (data/sat.db)│
│ - satellites + FTS5 │
│ - tle_current/history│
│ - launches/reentries│
│ - conjunctions │
│ - pass_cache │
│ - ground_stations │
└──────────┬──────────┘
▲
│ idempotent UPSERT
│
┌────────────────────────────┴────────────────────────────────┐
│ Cron-driven ingest CLI │
│ │
│ make ingest (CelesTrak GP, hourly) │
│ make ingest-satcat (daily) │
│ make ingest-ll2 (hourly upcoming, 6-hourly previous) │
│ make ingest-spacetrack (12-hourly) │
│ make ingest-socrates (8-hourly) │
│ make ingest-swpc (every 5 min) │
│ make ingest-ovation (every 15 min) │
│ make ingest-satnogs (weekly) │
│ make ingest-satnogs-stations (weekly) │
└────────────────────────────┬────────────────────────────────┘
▲
│ HTTPS
│
┌────────────────────────────┴────────────────────────────────┐
│ External upstreams (read-only fetch, idempotent retry) │
│ - celestrak.org (TLEs + SATCAT) │
│ - www.space-track.org (TIP decay messages, cookie-jar) │
│ - ll.thespacedevs.com (launches) │
│ - celestrak.org/SOCRATES (conjunctions) │
│ - services.swpc.noaa.gov (space weather + aurora) │
│ - db.satnogs.org + network.satnogs.org (radio + stations) │
└──────────────────────────────────────────────────────────────┘
The project is deliberately monolithic. There is one PHP process serving everything (API + text views + SPA shell) plus a set of CLI ingesters. There are no microservices, no message queues, no shared caches across machines.
| Boundary | Trust level | Notes |
|---|---|---|
GET /api/v1/* + GET /text/* |
public read-only | No authentication required |
POST /webhooks/ingest/{source} |
bearer-token auth | Off by default (empty WEBHOOK_SECRET → 403 always) |
bin/console * CLI |
shell access required | Runs as the system user; reads .env.{dev,prod} |
| SQLite file | filesystem permissions | Single-writer; cron-jobs serialize naturally |
- Read path: Browser → Apache/PHP-FPM → Slim front controller → Service container → SQLite → JSON or HTML
- Write path (cron): Cron →
php bin/console ingest:*→ external HTTPS fetch → parse → UPSERT into SQLite - Write path (webhook, optional): External signal →
POST /webhooks/ingest/{source}→ bearer-auth + rate-limit →proc_openasync dispatch to the same ingest CLI
src/App/Container.php— the PHP-DI service factory. Every service has one entry here.src/App/Kernel.php— the route table.src/Http/Controllers/*— per-endpoint controllers, one class per route.src/Ingest/*— one client (HTTP layer) + one ingester (parsing + UPSERT) per upstream.src/Services/*— pure-PHP business logic (PassCalculator, OgImageGenerator, etc.).resources/js/— Lit 3 components (<sat-*>elements), Cesium glue layers, satellite.js worker.resources/views/shell.php— the SPA HTML shell (loads Vite-built assets).
| Component | Version | Purpose |
|---|---|---|
| Lit | 3.2.x | Web Components (sat-* custom elements with shadow DOM) |
| Cesium.js | 1.121.x | WebGL globe + camera + skybox + lighting |
| satellite.js | 5.x | SGP4 propagation (in a Web Worker) |
| TypeScript | 5.6 (strict) | Type system |
| Vite | 8.0.x | Build tooling + dev HMR |
| Vitest | 4.1.x | Unit + UI logic tests |
| ESLint | 9.x (flat config) | + typescript-eslint |
| Prettier | 3.x | Formatting |
| vite-plugin-cesium | 1.2.x | Cesium asset wiring |
| Component | Version | Purpose |
|---|---|---|
| PHP | 8.4 | Backend runtime (readonly + property hooks) |
| Slim Framework | 4.13.x | Front controller + router |
| PHP-DI | 7.x | Service container |
| Eloquent (illuminate/database) | 11.x | Query builder + connection pool |
| illuminate/console | 11.x | Migrations + bin/console CLI |
| Monolog | 3.x | Logging |
| Guzzle | 7.x | HTTP client (ingesters) |
| vlucas/phpdotenv | 5.x | .env loading |
| zircote/swagger-php | 6.x | OpenAPI generation |
| Component | Version | Purpose |
|---|---|---|
| SQLite | 3.40+ (via pdo_sqlite) |
All persistence — single file at data/sat.db |
| FTS5 | (sqlite built-in) | Catalog search |
| Component | Recommended | Notes |
|---|---|---|
| DreamHost VPS | 1 GB / 1 vCPU | The canonical production target |
| Fly.io | shared-cpu-1x@256MB | Alternative documented in docs/deploy.md |
| Apache | 2.4+ with mod_rewrite |
Production web server |
| PHP-FPM | 8.4 | Process manager |
| cron | any | Drives every ingester |
| Tool | Version | Purpose |
|---|---|---|
| PHPUnit | 11.x | 411 PHP tests |
| Vitest | 4.1.x | 361 JS tests |
| Playwright | 1.60.x | 146 e2e tests + 3 skipped |
| PHPStan | level 6 | Static analysis (with a frozen baseline) |
| PHP-CS-Fixer | 3.x | PSR-12 + declare(strict_types=1) enforcement |
- PHP 8.4 with
pdo_sqlite+curl+gd+mbstring+json. APCu recommended. - Node 20+ with npm 10+ for the Vite build and the
bin/sgp4-passes.mjsCLI. - SQLite 3.40+ (uses
RETURNINGclauses).
php --version # → PHP 8.4.x
node --version # → v20.x or later
npm --version # → 10.x or later
composer --version # → Composer 2.x
sqlite3 --version # → 3.40 or later
make --version # any
git --version # any
php -m | grep -E 'pdo_sqlite|curl|gd|mbstring|json' # all five must printIf any command fails, install the missing tool via your OS package manager:
| OS | Command |
|---|---|
| Debian / Ubuntu | sudo apt install php8.4 php8.4-{sqlite3,curl,gd,mbstring} composer nodejs npm sqlite3 make |
| macOS (Homebrew) | brew install php@8.4 node composer sqlite make (then brew link php@8.4) |
| Arch Linux | sudo pacman -S php php-sqlite php-gd composer nodejs npm sqlite make |
| DreamHost VPS | PHP 8.4 + extensions selected via the control panel; Node via nvm; rest via apt |
git clone git@github.com:CyberSecDef/sat.trackr.live.git
cd sat.trackr.livecp .env.example .env # base config (APP_ENV, APP_NAME, APP_URL, LOG_LEVEL)
cp .env.dev.example .env.dev # dev overlay (DB_PATH, VITE_DEV_ORIGIN, optional API creds)
# Later, for production:
# cp .env.prod.example .env.prodReal .env* files are gitignored. Edit .env.dev to set any optional API credentials (SPACE_TRACK_USER/PASS, N2YO_API_KEY, LL2_API_TOKEN); leave them blank to disable the corresponding ingesters.
make install # composer install + npm install (~30 s)
make build # vite build → public/build/ (~5 s)make migrate # applies 16 migrations
make migrate-status # verify: should show all applied, 0 pendingmake ingest # CelesTrak GP — ~15,700 satellites, ~40 s (REQUIRED)
make ingest-satcat # enrichment (optional, ~30 s)
make ingest-ll2 # upcoming + recent launches (optional, ~4 s)
make ingest-socrates # close-approach predictions (optional, ~12 s)
make ingest-swpc # space weather (optional, < 1 s)
make ingest-ovation # aurora forecast (optional, < 1 s)
make ingest-satnogs # amateur-radio transmitters (optional)
make ingest-satnogs-stations # amateur ground stations (optional)make health
# Expect: PHP version + ext: pdo_sqlite + DB connection + migration status +
# table: satellites with 15,000+ rows, all reporting "ok".| Symptom | Likely cause | Fix |
|---|---|---|
composer install 404 on a package |
composer.lock vs registry mismatch | composer clear-cache && composer install |
npm install fatal native-build error |
Old Node version | Upgrade to Node 20+ |
make migrate "no such file" |
DB_PATH directory doesn't exist |
mkdir -p data |
make ingest 403 |
CelesTrak's "not modified" signal (safe to ignore on re-runs) | — |
make ingest-spacetrack 401 |
Missing or wrong SPACE_TRACK_USER / SPACE_TRACK_PASS |
Re-check .env.dev |
make health reports 0 satellites |
make ingest hasn't run |
Run step 6 |
The fastest path from git clone to a working app on a fresh machine with prerequisites already in place:
git clone git@github.com:CyberSecDef/sat.trackr.live.git
cd sat.trackr.live
cp .env.example .env && cp .env.dev.example .env.dev
make install build migrate
make ingest # ~40 s; the globe is empty without this
make serve # PHP server on 0.0.0.0:8000Expected outcome (~3 minutes total): browser hits http://localhost:8000/ and renders a globe with ~15,700 cyan dots. Searching for ISS (or 25544) opens the detail panel.
Smoke checks (run after make serve is running):
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/ # → 200
curl -s http://localhost:8000/api/v1/satellites?limit=1 | head -c 200 # → JSON envelope
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/text # → 200
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/events.atom # → 200For hot-module-reload during development, replace make serve with make dev — it starts PHP + Vite in parallel on 0.0.0.0:8000 and 0.0.0.0:5173.
Every key referenced by EnvLoader::get() in PHP. The base .env holds environment-agnostic values; .env.{dev,prod} overlays the environment-specific ones (DB paths, credentials). Templates: .env.example, .env.dev.example, .env.prod.example. Real .env* files are gitignored.
| Key | Required? | Default | Which feature | Where it's read |
|---|---|---|---|---|
APP_ENV |
no | dev |
Selects which overlay file (.env.dev or .env.prod) loads after .env |
src/App/EnvLoader.php, src/App/Container.php |
APP_NAME |
no | sat.trackr.live |
<title>, OG meta tags, footer credit |
src/App/Container.php |
APP_URL |
recommended in prod | http://localhost:8000 |
Absolute URLs in OG meta, sitemap, Atom feed entries | src/App/Container.php, src/Http/Controllers/AtomEventsController.php |
LOG_LEVEL |
no | info |
Monolog verbosity (debug / info / notice / warning / error / critical) |
src/App/Container.php |
VITE_DEV_ORIGIN |
no | http://localhost:5173 |
Origin of the Vite dev server; injected into the HMR client URL during make dev |
src/App/Container.php |
DB_PATH |
yes (in prod) | data/sat.db |
SQLite database file location. Use an absolute path in prod | src/App/Container.php |
CESIUM_ION_TOKEN |
optional | empty | Cesium ion-hosted imagery + terrain. Empty falls back to OpenStreetMap | src/App/Container.php |
NODE_BINARY |
optional | node |
Absolute path to the Node binary bin/sgp4-passes.mjs shells out to. Set when node isn't on the cron $PATH |
src/App/Container.php |
SPACE_TRACK_USER |
optional | empty | Space-Track.org username for TIP decay-message ingest | src/App/Container.php |
SPACE_TRACK_PASS |
optional | empty | Space-Track.org password — paired with SPACE_TRACK_USER |
src/App/Container.php |
N2YO_API_KEY |
optional | empty | N2YO API key for pass-prediction visual-magnitude enrichment | src/App/Container.php |
LL2_API_TOKEN |
optional | empty | Launch Library 2 paid-tier token. Free tier (15 req/hr) works without one | src/App/Container.php |
PLAUSIBLE_DOMAIN |
optional | empty | Cookieless Plausible analytics data-domain attribute. Empty omits the script entirely |
src/App/Container.php |
PLAUSIBLE_SCRIPT_URL |
optional | https://plausible.io/js/script.js |
Plausible script source — change for self-hosted instances | src/App/Container.php |
INGEST_ALERT_WEBHOOK_URL |
optional | empty | Slack/Discord/Mattermost-compatible incoming webhook for TLE reject-rate alerts | src/App/Container.php |
WEBHOOK_SECRET |
optional | empty | Bearer-token shared secret for POST /webhooks/ingest/{source}. Empty → endpoint returns 403 always |
src/App/Container.php |
Security-sensitive keys: SPACE_TRACK_PASS, N2YO_API_KEY, LL2_API_TOKEN, INGEST_ALERT_WEBHOOK_URL, WEBHOOK_SECRET. Never commit any of these. The .env.prod.example template ships with empty values; populate the real .env.prod only on the deploy target.
Feature toggles: every "optional" key disables the corresponding feature gracefully when empty (no errors). For example, WEBHOOK_SECRET="" makes the push-based ingest endpoint return 403 on every request without crashing.
# Which environment overlay to load: dev or prod.
APP_ENV=dev
# Human-readable name (used in <title>, OG tags, footer)
APP_NAME="sat.trackr.live"
# Base URL where this app is served. No trailing slash.
APP_URL="http://localhost:8000"
# Log verbosity: debug | info | notice | warning | error | critical
LOG_LEVEL=infoSee .env.dev.example and .env.prod.example for the overlay-specific keys (DB paths, API credentials).
# Catalog ingest
make ingest # all 38 CelesTrak GP groups
make ingest-group GROUP=starlink # single group
make ingest-satcat # SATCAT enrichment
# Database ops
make migrate-status # see applied/pending migrations
make make-migration NAME=add_new_column # generate a migration skeleton
make pass-cache-prune # sweep expired rows
# Quality + security
make ci # full quality gate
make test-e2e # Playwright smoke
make security-audit # blocking gate
make security-audit-dump # verbose summary
make health # DB + migration sanity check# Catalog list (paginated)
curl -s 'http://localhost:8000/api/v1/satellites?per_page=5' | jq '.data[0]'
# Per-satellite detail
curl -s http://localhost:8000/api/v1/satellites/25544 | jq
# Pass predictions from an observer location
curl -s 'http://localhost:8000/api/v1/satellites/25544/passes?lat=51.5074&lon=-0.1278' | jq '.data[0]'
# Current state-now (server-side propagation, 60 s cache)
curl -s 'http://localhost:8000/api/v1/satellites/25544/state-now?lat=51.5&lon=-0.1' | jq
# ICS calendar export for upcoming passes
curl -s 'http://localhost:8000/api/v1/satellites/25544/passes.ics?lat=51.5&lon=-0.1' > iss.ics
# Event syndication (Atom 1.0)
curl -s http://localhost:8000/events.atom | head -40
# Upcoming launches
curl -s http://localhost:8000/api/v1/launches/upcoming | jq '.data[0]'
# Upcoming conjunctions
curl -s 'http://localhost:8000/api/v1/conjunctions/upcoming?within_hours=72' | jq '.data[0]'Find the ISS and watch it overhead:
- Open
http://localhost:8000/(or the LAN URL). - Click
📍 set locationin the topbar; allow geolocation (or pick a city). - Press
⌘K(orCtrl+K); typeissor25544; press Enter. - The detail panel opens with the ISS selected. Scroll to
§ Visibility from observer. - The next 5 visible passes appear with rise/peak/set times.
- Click the
↑ Sky viewtopbar toggle to flip into stargazer mode.
Replay a conjunction:
- Visit
/text/conjunctionsto browse the SOCRATES table. - Click any high-probability row; the replay route opens at
/conjunction/{primary}/{secondary}. - The Cesium camera flies alongside the two objects through their time of closest approach.
Share a deep-link:
http://localhost:8000/?sat=25544— opens the SPA with the ISS pre-selected.http://localhost:8000/?sat=25544&lat=51.5&lon=-0.1&view=sky— drops the recipient directly into the sky-view planetarium pose at the observer's coordinates.
- No WebGL? Visit
/textdirectly. Every meaningful surface has a server-rendered HTML mirror that works without JavaScript. - Bad signal on mobile? The PWA service worker caches
/textfor offline access after a successful first load. /events.atomfor RSS readers? Subscribe in your feed reader; ~32 KB cap, refreshed every 10 minutes.
- OpenAPI 3.1 spec:
GET /api/v1/openapi.json(auto-generated from controller attributes) - Swagger UI:
GET /api/v1/docs(interactive browser) - Static dump:
make openapi-dumpregeneratespublic/openapi.json(committed; useful for CI consumers without a running server)
GET /api/v1/*— public, no authentication required. CORS is open; ETag + JSON middleware applied.GET /text/*— public, no authentication required.POST /webhooks/ingest/{source}— bearer-token auth (Authorization: Bearer <WEBHOOK_SECRET>). Off by default; emptyWEBHOOK_SECRETreturns 403 on every request before the source allowlist check.
| Group | Path | Purpose |
|---|---|---|
| Catalog | GET /api/v1/satellites |
Paginated catalog list |
GET /api/v1/satellites/{norad} |
Per-satellite detail | |
GET /api/v1/satellites/{norad}/tle |
Raw TLE lines | |
GET /api/v1/satellites/{norad}/passes |
Pass predictions (observer-aware) | |
GET /api/v1/satellites/{norad}/passes.ics |
ICS calendar export | |
GET /api/v1/satellites/{norad}/state-now |
Server-side propagated state | |
GET /api/v1/satellites/{norad}/radio |
SatNOGS transmitter list | |
| Search | GET /api/v1/search?q=… |
Catalog search (FTS5-backed) |
GET /api/v1/autocomplete?q=… |
Autocomplete suggestions | |
| Groups | GET /api/v1/groups |
All CelesTrak groups |
GET /api/v1/groups/{slug} |
Group metadata + members | |
GET /api/v1/groups/{slug}/tles |
Raw TLEs for the group | |
| Launches | GET /api/v1/launches/upcoming |
Next ~50 launches |
GET /api/v1/launches/recent |
Last ~100 launches | |
GET /api/v1/launches/{id} |
Per-launch detail | |
GET /api/v1/launch-sites |
All known pads | |
| Reentries | GET /api/v1/reentries/upcoming |
TIP-predicted reentries |
GET /api/v1/reentries/{norad} |
Per-NORAD TIP detail | |
GET /api/v1/reentries/traces?days=30 |
Last 30 d of decay-trace propagations | |
| Conjunctions | GET /api/v1/conjunctions/upcoming |
SOCRATES close-approaches |
GET /api/v1/conjunctions/{primary}/{secondary} |
Per-event detail | |
| Space weather | GET /api/v1/space-weather/now |
Current Kp + X-ray |
GET /api/v1/space-weather/24h |
Last 24 h history | |
| Stats | GET /api/v1/stats/{breakdown} |
Operator/country/type/year aggregates |
| Stations | GET /api/v1/amateur-stations |
SatNOGS ground stations |
| Constellations | GET /api/v1/gnss/membership |
GPS/Galileo/GLONASS/BeiDou NORAD lists |
| Sky view | GET /api/v1/sky-view/passes |
Aggregate pass-arc data for the planetarium overlay |
| Events | GET /events.atom |
Atom 1.0 syndication (launches + reentries + conjunctions + storms) |
| OG | GET /og/{type}/{id}.png |
Open Graph image generation |
| Spec | GET /api/v1/openapi.json |
OpenAPI 3.1 |
GET /api/v1/docs |
Swagger UI | |
| Webhook | POST /webhooks/ingest/{source} |
Push-based ingest receiver |
The API is prefixed /api/v1/. Breaking changes — schema removals, semantic changes to existing fields, contract changes — would land under /api/v2/ rather than mutating /v1/. Additive changes (new optional fields, new endpoints) ship under the existing /v1/ prefix.
All JSON endpoints return either:
- Single resource: the resource object directly, e.g.
{ "norad_id": 25544, "name": "ISS (ZARYA)", … } - List resource: an envelope:
{ "data": [...], "meta": { "page": 1, "per_page": 100, "total": 15665, "total_pages": 157 }, "links": { "next": "...", "prev": null } }
{
"error": "human-readable message",
"status": 400
}HTTP status codes: 400 validation, 403 webhook auth, 404 resource not found, 405 method not allowed, 429 webhook rate-limited (Retry-After header set), 500 internal error.
- Read API: no rate limit (the canonical assumption is "small number of legitimate consumers; a CDN handles the scale-out").
- Webhook: 1 request per
(source, IP)per 60 seconds. State persisted tostorage/cache/webhook-cooldown.json(atomic tmp+rename writes for PHP-FPM safety).
# Example — trigger a CelesTrak ingest via push notification:
curl -X POST \
-H "Authorization: Bearer $WEBHOOK_SECRET" \
https://example.com/webhooks/ingest/celestrak
# 202 Accepted with:
# { "status": "accepted", "source": "celestrak", "command": "ingest:celestrak" }Source allowlist: celestrak, spacetrack, ll2, socrates, swpc, ovation, satnogs, satnogs-stations.
This is a read-only public website with no user accounts and no user-supplied data persisted server-side. The realistic threats are:
- Supply-chain compromise — a malicious PHP composer dep or npm package landing during install.
- API abuse — high-frequency scraping affecting the SQLite single-writer or the upstream rate budgets.
- Webhook abuse — a leaked
WEBHOOK_SECRETletting an attacker trigger cron-style ingest commands. - XSS via uncontrolled satellite metadata — an attacker pushing malicious strings into Space-Track / CelesTrak that render unescaped in the SPA.
- Read API: none. The catalog is public.
- Webhook:
Authorization: Bearer <WEBHOOK_SECRET>. Constant-time comparison (hash_equals). Default-off (empty secret → 403 always, before the source check, so a leaked secret can't leak the allowlist shape). - CLI: filesystem-level (the system user running cron).
- In transit: TLS at the deploy boundary (Apache + Let's Encrypt, or Fly.io's load balancer). The app does not handle TLS termination directly.
- At rest: SQLite is plaintext on disk; rely on disk-level encryption (LUKS, FileVault, etc.) for the host.
- Secrets: env vars in
.env.prod, which is gitignored. Never logged. Never echoed to stdout.
- All secrets live in
.env.prodon the deploy host, not in the repo. bin/security-audit-dump.shrunscomposer audit,npm audit, and queries GitHub's Dependabot / CodeQL / secret-scanning APIs (whenghis authenticated) to surface drift.bin/security-audit-gate.sh(viamake security-audit) exits non-zero if any finding is≥ moderate.make cichainsmake security-audit-advisory(non-blocking variant) per the locked Phase 11 rollout policy.
| Layer | Tool | Cadence |
|---|---|---|
| PHP deps | composer audit |
Every make ci (advisory); manual via make security-audit |
| JS deps | npm audit |
Same |
| Dependency graph | GitHub Dependabot | Continuous |
| PHP static analysis | PHPStan level 6 | Every make ci |
| PHP code style | PHP-CS-Fixer (PSR-12 + strict_types) |
Every make ci |
| JS lint | ESLint flat config + typescript-eslint | Every make ci |
| CodeQL (TypeScript/JS) | .github/workflows/codeql.yml |
Per push |
| SonarQube (PHP + TS/JS) | .github/workflows/sonarqube.yml |
Per push |
- Apache: ship the project with
public/.htaccessenabled — it adds CSP-friendly security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy). - PHP-FPM: run the pool as a non-root user.
- SQLite file: chmod 640 (PHP-FPM read + write, web user no access).
- No public webhook? Leave
WEBHOOK_SECRET=""— the endpoint stays off. - Rotating the webhook secret: generate with
openssl rand -hex 32. There is no revocation list — rotate by changing the env var and reloading PHP-FPM.
- CodeQL does not scan PHP. The bundled
.github/workflows/codeql.ymlcovers TypeScript/JS. PHP-side static analysis is via PHPStan locally + SonarQube in CI. - No API-wide rate limiting. Phase 12+ candidate.
- No CSP
script-src 'strict-dynamic'enforcement — Cesium's inline shaders make a strict CSP non-trivial; deferred. - Subresource Integrity (SRI) for CDN-loaded Swagger UI — not yet implemented; Cesium's version churn complicates the maintenance.
Report security issues via a GitHub issue marked security or via the contact listed in composer.json support. Public disclosure is the default; coordinated disclosure for genuinely exploitable issues is welcome — the project author will respond within ~72 hours.
- Behind HTTPS (Let's Encrypt or Fly LB).
WEBHOOK_SECRETset only if push-based ingest is wanted; otherwise leave empty (the default).- Cron user is non-root.
make security-auditwired into your CI workflow (the Makefile target exists; the GitHub Actions wrapper is a future task).
make dev # PHP + Vite HMR on 0.0.0.0:{8000,5173}
# or
make serve # PHP only on 0.0.0.0:8000, serving the pre-built SPAThe authoritative deployment guide is docs/deploy.md — walks two production paths end-to-end:
- DreamHost VPS (Apache + cron) — the canonical target. Apache vhost config, PHP-FPM selection, document-root pointing, cron entries with
MAILTO, log rotation. - Fly.io (single machine + scheduled cron) — sample
fly.toml,Dockerfile,.fly.dockerignore, two cron strategies (in-containercrondvs scheduled-worker), troubleshooting matrix.
The Fly.io path's Dockerfile in docs/deploy.md works as a standalone Docker baseline:
docker build -t sat-trackr-live .
docker run -p 8080:8080 \
-v "$PWD/data:/app/data" \
-e APP_ENV=prod \
-e DB_PATH=/app/data/sat.db \
sat-trackr-liveApache via public/.htaccess is the supported path. For Nginx or Caddy, the rewrite rule is "everything not a real file → /index.php":
location / {
try_files $uri $uri/ /index.php?$query_string;
}- DreamHost VPS — recommended for shared-hosting comfort.
- Fly.io — recommended for Docker-comfort + multi-region edge cache.
- AWS / GCP / Azure — works with any PHP-FPM-compatible host; no project-specific quirks.
- Vertical first. A single 1 GB / 1 vCPU machine handles the current catalog comfortably.
- CDN in front (CloudFlare, Fastly, Bunny) for
/api/v1/*GETs — every JSON response isCache-Control: max-age=Ntaggable. - Horizontal scale-out would require migrating SQLite → Postgres (single-writer becomes the bottleneck under sustained write traffic). Out of scope for the current product.
The project is intentionally single-replica. Failover story is "rebuild from git pull + cron-driven re-ingest." Recovery time on a fresh VPS: ~10 minutes (clone + install + migrate + ingest).
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 1 vCPU | 1 vCPU |
| RAM | 256 MB | 512 MB |
| Disk | 500 MB | 2 GB (room for logs + Cesium assets + marquee glTFs + DB growth) |
| Network | outbound HTTPS to upstreams | same + sufficient bandwidth for Cesium asset delivery (~250 MB initial) |
| Path | Size |
|---|---|
data/sat.db (SQLite + WAL) |
~50 MB |
public/cesium/ (runtime assets) |
~250 MB |
public/textures/skybox/ (BSC5 cubemap) |
~120 KB |
public/textures/earth-at-night.jpg (VIIRS) |
~800 KB |
public/models/*.glb (optional marquee glTFs) |
~45 MB |
storage/logs/ (rotating) |
~10-50 MB depending on LOG_LEVEL |
vendor/ (composer) |
~30 MB |
node_modules/ (dev only) |
~400 MB |
- Read API: SQLite handles ~10K read-QPS on commodity SSD; the project's realistic load is < 100 RPS sustained even at heavy traffic. CDN caching multiplies headroom.
- Cron writes: every ingester is single-process, sequential, and serialized at the SQLite-WAL layer. No write contention.
- Cesium first-load: ~3-5 MB of JS + ~250 MB of WebGL assets (lazy-loaded). Mitigate with CDN caching headers (already set).
Not formally benchmarked under load. The flake-floor work in tests/E2E/FLAKINESS.md includes per-spec wall-time observations (~52 s for the full 146-test Playwright suite).
- SQLite single-writer. If write QPS becomes a concern, migrate to Postgres (out of scope).
- PHP-FPM worker count scales horizontally on a single host; tune
pm.max_childrento RAM. - Cesium asset delivery is the realistic bottleneck under load; a CDN in front is the recommended scaling lever.
| Source | Path | Format |
|---|---|---|
| Application | storage/logs/app.log (Monolog rotating) |
JSON-or-text per LOG_LEVEL |
| Cron output | storage/logs/cron.log |
Whatever the ingesters print (line-oriented) |
| Webhook dispatch | storage/logs/webhook-ingest.log |
Whatever the dispatched CLI prints |
| Apache/PHP-FPM access | /var/log/apache2/access.log (or vhost-specific) |
Combined |
# .env or .env.{dev,prod}
LOG_LEVEL=info # debug | info | notice | warning | error | criticalNo formal metrics endpoint (Prometheus, OpenTelemetry, etc.). The project's observability story is:
make health— DB ping, migration status, row counts (operator-facing).GET /api/v1/satellites?limit=1— HTTP smoke (200 + JSON envelope).- Plausible — opt-in cookieless web analytics (per-page-view counts; no individual user data).
| Check | What it verifies |
|---|---|
make health |
PHP version + ext: pdo_sqlite + DB connection + migrations applied + table row counts |
curl /api/v1/satellites?limit=1 |
HTTP listener up + DB queryable |
curl /text |
SSR pipeline + asset serving |
- Plausible (cookieless) — set
PLAUSIBLE_DOMAINto enable; respects DNT header server-side +localStorage.sat:disableAnalytics='1'opt-out. - Slack/Discord/Mattermost webhook for TLE reject-rate alerts — set
INGEST_ALERT_WEBHOOK_URL. Thresholds: CelesTrak 1 %, SpaceTrack 5 %, LL2 10 %, SOCRATES 5 %. 30-min cooldown per(ingester, band).
Logs are line-oriented and ship in standard locations:
# Example filebeat config snippet
filebeat.inputs:
- type: filestream
paths:
- /home/user/sat.trackr.live/storage/logs/*.log
- /var/log/apache2/access.log- Ingest reject-rate is logged for every ingester run; webhooks fire when the ratio crosses per-ingester thresholds.
- Webhook dispatch is logged per-request to
storage/logs/webhook-ingest.log(source, IP, dispatch outcome). - No PII is logged anywhere — the observer location stays in the browser's
localStorageand is never sent to the server outside of optional?lat&lonquery parameters on a small subset of endpoints.
| Layer | Tool | Count | What it covers |
|---|---|---|---|
| Unit (PHP) | PHPUnit 11 | ~250 | Pure-PHP logic: TLE parsing, magnitude estimation, schema, etc. |
| Feature (PHP) | PHPUnit 11 (real SQLite) | ~160 | API routes, controllers, ingesters end-to-end against a real test DB |
| Unit (JS) | Vitest 4 | 361 | Pure-math: SGP4 helpers, look-angles, replay-window, sort-hijack |
| E2E | Playwright 1.60 | 146 + 3 skipped | Full SPA + /text flows in headless Chromium |
| Static (PHP) | PHPStan level 6 | — | Type and null-safety enforcement (with a frozen baseline) |
| Style (PHP) | PHP-CS-Fixer 3 | — | PSR-12 + strict_types enforcement |
| Lint (JS) | ESLint 9 flat config + typescript-eslint | — | TS strict + plugin set |
make test # PHPUnit + Vitest sequentially
make test-php # PHPUnit only
make test-js # Vitest only
make test-e2e # Playwright (needs `npx playwright install chromium` once)
make ci # full quality gate: lint + analyze + typecheck + test + security-audit-advisoryCoverage is not currently tracked. PHPUnit + Vitest emit coverage data on --coverage flags; no CI artifact upload is configured.
- PHPUnit feature tests run against a real SQLite database (in-memory or per-test temp file) so SQL is exercised end-to-end.
- Ingester tests stub HTTP via Guzzle's
MockHandlerrather than mocking the ingester itself. - Playwright runs against the PHP dev server (
make serve-style) onlocalhost:8000. No staging environment is currently provisioned.
make ciis the local quality gate and what an automated CI should run.make security-audit-advisoryis chained intomake ciper a 30-day non-blocking rollout policy; future PR will flip to blocking after the rollout window.- The bundled
.github/workflows/codeql.ymland.github/workflows/sonarqube.ymlrun static analysis per push.
tests/E2E/FLAKINESS.md is the institutional-memory record. After Phase 11 chunk 4 the suite reached 0 fails across 10 sweep runs (the ≤ 1/10 acceptance floor). Re-running the audit is documented in the ledger.
Not currently performed. The project does not have a load-test harness; under the catalog's current cardinality and a CDN in front, real-world load is comfortably below SQLite's read budget.
Q: The globe is empty / no satellites visible.
A: Did you run make ingest? make health should report ≥ 15,000 rows in the satellites table. If it reports 0, ingest didn't run — check for HTTP errors in the output of make ingest directly.
Q: "WebGL not supported" — what now?
A: /text is a full server-rendered HTML fallback. Bookmark it instead. The SPA gates on hasWebGL() and falls back automatically with a CTA pointing at /text.
Q: Cesium imagery is gray / blank.
A: Either set CESIUM_ION_TOKEN (free from cesium.com/ion/signup) or accept the OpenStreetMap fallback (the default — works fully but less polished).
Q: make ingest-spacetrack fails with 401.
A: Wrong or missing SPACE_TRACK_USER / SPACE_TRACK_PASS in .env.dev. Test the creds at https://www.space-track.org/auth/login directly.
Q: make ingest returns 403.
A: That's CelesTrak's "not modified" response — safe to ignore on re-runs. Run with --force if you really want to bypass the cache (the CLI flag forwards to bin/console).
Q: SQLite "locked" errors during ingest.
A: Another process is holding the DB. Check for stuck PHP processes: ps aux | grep php. Stop them and retry. Multiple cron jobs running simultaneously can also trigger this — stagger their schedules.
Q: node not found when cron runs make pass-cache-prune.
A: nvm-installed Node isn't on cron's $PATH. Set NODE_BINARY=/full/path/to/node in .env.prod.
Q: The page shows stale assets after a deploy.
A: The service worker caches Vite-built assets aggressively. After deploy, open DevTools → Application → Service Workers → "Unregister", then hard-reload. The chunk-4 helpers expose cleanBrowserStorage(page) for Playwright tests to do the same.
Q: PHPStan reports 36 errors I didn't introduce.
A: Those are baselined in phpstan-baseline.neon — pre-existing debt frozen at the Phase 11 close. They shouldn't fail make ci. If they do, the baseline file may have been deleted; restore it from git history.
Q: PHP-CS-Fixer reports drift on files I didn't touch.
A: Run make lint-fix to auto-fix. The fixes are deterministic style normalizations; review the diff and commit.
make health # DB + migrations + table counts
sqlite3 data/sat.db 'SELECT COUNT(*) FROM satellites;' # raw row count
sqlite3 data/sat.db .schema # full DB schema
bin/security-audit-dump.sh # verbose security tool summary
php -m | sort # loaded PHP extensions
composer audit # PHP dep vulnerabilities
npm audit # JS dep vulnerabilitiesmainis the default branch and the only long-lived branch.- Feature branches:
feature/<short-name>orchore/<short-name>. - Squash-merge into
main. Avoid rebase-and-merge unless the branch has a meaningful per-commit history worth preserving.
make ciruns green locally (lint + analyze + typecheck + tests + security-audit-advisory).- New behavior is covered by a test (PHPUnit / Vitest / Playwright as appropriate).
- Commit messages explain why, not what (the diff already shows what).
- PR description includes a "How to verify" section if the change isn't obvious from the diff.
| Language | Standard |
|---|---|
| PHP | PSR-12, declare(strict_types=1), PHPStan level 6, PHP-CS-Fixer |
| TypeScript | strict mode, noUnusedLocals, noUnusedParameters, noImplicitReturns |
| CSS | Per-component scoped styles inside Lit elements; theme tokens in resources/css/themes/ |
| Shell | #!/usr/bin/env bash, set -uo pipefail |
make lint # check
make lint-fix # auto-fix everything fixableFree-form subjects, but the project uses a "Phase N chunk M: short summary" convention for chunked work (see git log). Body explains why; trailers include Co-Authored-By: when relevant.
- One reviewer minimum on substantial PRs; trivial doc fixes can self-merge.
- Reviewer runs
make cilocally before approving. - Authors fix review feedback in fresh commits (squashed at merge), not amends.
- Phase-based development. Phases 1-11 are complete (67 chunks total).
docs/phase_summary.mdis the historical chunk-by-chunk record;docs/phase{1..11}.mdpreserve the per-phase design intent. - No SemVer.
package.jsonships at0.0.0;composer.jsondoes not have a version key. The project is hobby-maintenance-mode and doesn't ship versioned releases.
- API:
/api/v1/*is the public surface. Additive changes (new optional fields, new endpoints) land in/v1/. Breaking changes would go to/v2/(none planned). - Database schema: migrations are forward-only via
make migrate.make rollbackrolls back the most recent batch (use sparingly). - CLI:
bin/consolesubcommand names are stable; flags can be added but not removed without a deprecation cycle.
A breaking API change would require:
- Land the new behavior at
/api/v2/. - Document the migration path in
CHANGELOG.md(file doesn't exist yet — a future chunk would create it). - Leave
/v1/running unchanged for at least 90 days post-/v2/launch.
No formal policy. The project follows an "if it breaks, fix it forward" stance compatible with a single-maintainer hobby project. Heavy users should pin to a specific commit SHA via git submodule or a vendored fork.
cd /path/to/deploy
git pull
make install-prod # composer install --no-dev + npm ci
make build # vite build
make migrate # apply any new migrations
# Reload PHP-FPM:
sudo systemctl reload php8.4-fpm
make health # verifyAGPL-3.0-or-later. See LICENSE (if present) or https://www.gnu.org/licenses/agpl-3.0.html.
If you run a modified version of this software on a public service, you must offer the modified source to your users. This is the central AGPL difference from GPLv3 and a deliberate choice — the project's premise (open public-orbit data) carries naturally to its license.
| Component | License |
|---|---|
| Cesium.js | Apache 2.0 |
| Lit | BSD-3-Clause |
| satellite.js | MIT |
| Slim Framework | MIT |
| PHP-DI | MIT |
| Eloquent / illuminate | MIT |
| Monolog | MIT |
| Guzzle | MIT |
| Vite | MIT |
| Vitest | MIT |
| Playwright | Apache 2.0 |
| PHPUnit | BSD-3-Clause |
| PHPStan | MIT |
| Marquee glTF models | Various (see public/models/CREDITS.md for per-model attribution) |
A full transitive license audit is not provided — composer and npm both surface license metadata; run composer licenses and npm-license-checker for the current export.
None known. The project transmits and processes publicly-available orbital data sources (CelesTrak, Space-Track, NOAA). Operators are responsible for their own jurisdictional compliance.
"sat.trackr.live" is not a registered trademark. Derivative works are not required to rename, but should clearly attribute the upstream (a Based on sat.trackr.live line in the footer or about page is the courtesy expectation).
- AGPL distribution — preserve the
LICENSEfile + the original copyright notice. - API consumers — attribution is appreciated but not required for read-only API use.
- Embedded screenshots / globe captures — attribution is appreciated (
Image: sat.trackr.live).
Maintenance mode as of the Phase 11 close (2026-05-23). The project is feature-complete across the 11 planned phases: foundation, data depth, showcase visuals, situational awareness, polish and ecosystem, conjunction-replay showcase, sky view, engagement and accessibility, visual depth, ops and scale, and production-grade hygiene.
| Metric | Value |
|---|---|
| Total chunks delivered | 67 across 11 phases |
| PHPUnit tests | 411 |
| Vitest tests | 361 |
| Playwright tests | 146 passing + 3 skipped |
| Open security findings | 0 |
| Playwright flake floor | 0/10 (verified across a 10× sweep) |
| Bundle size (gzipped main) | 67.56 KB |
Bug fixes, security-audit follow-throughs, and small operational improvements continue. No active feature development. The project tracks Dependabot alerts and applies critical patches as they land.
| Item | Status | Notes |
|---|---|---|
| PHPStan baseline reduction | 📋 | The 36-error baseline frozen at Phase 11 close is the target |
| Fresh-VPS verification walkthrough | 📋 | An operator-driven literal-walk through the README + docs/deploy.md |
GitHub Actions integration for make security-audit |
📋 | Currently runs locally only; CodeQL + SonarQube workflows already landed but don't invoke make security-audit directly |
| Subresource Integrity (SRI) for CDN-loaded Swagger UI | 📋 | Deferred from Phase 11 § V |
| Strict Content Security Policy | 📋 | Deferred — Cesium's inline shaders make this non-trivial |
| Per-API rate limiting | 📋 | Phase 12+ candidate; would require a Redis or in-process counter |
| WebGPU instanced rendering | 📋 | Only matters when the dot count grows past ~50K |
CHANGELOG.md adoption |
📋 | Phase-based commits cover the same ground today |
None currently. All shipped phases remain in production use.
docs/req_spec.md is the long-form requirements document (§1-§30). It captures the project's original premise plus deliberate deferrals. Phases 1-11 implemented the bulk of the in-scope items; the deferred items there map to the planned-capabilities table above.
This is a single-maintainer hobby project released under AGPL-3.0-or-later. There is no SLA. The maintainer responds to issues on a best-effort basis. PRs are welcome and reviewed within a typical 1-2 week window.
- Bug reports: open a GitHub issue. Issues marked
securityget faster attention. - Feature requests: open a GitHub issue with the use case. The project author may decline if the feature is outside the locked vision in
docs/req_spec.md. - Forks: encouraged. AGPL grants the right to fork freely; if you run a modified public service, AGPL also requires you to publish your source.
This project would not exist without the public space-data ecosystem built and maintained by:
- CelesTrak — Dr. T.S. Kelso's TLE service, the de facto open standard.
- Space-Track.org — US Space Force / 18 SDS.
- The Space Devs / Launch Library 2 — community-maintained launch catalog.
- NOAA SWPC — space-weather + aurora-forecast products.
- SatNOGS — Libre Space Foundation's amateur-radio satellite network.
- Cesium — WebGL globe.
- satellite.js — TS Kelso's SGP4 in JavaScript.
And to everyone who's ever looked up.