Skip to content

CyberSecDef/sat.trackr.live

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

224 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sat.trackr.live

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.


Table of contents

  1. Executive summary
  2. Feature matrix
  3. Architecture overview
  4. Technology stack
  5. Installation
  6. Quick start
  7. Configuration
  8. Usage examples
  9. API documentation
  10. Security considerations
  11. Deployment
  12. Operational requirements
  13. Logging, monitoring, and telemetry
  14. Testing
  15. Troubleshooting and FAQ
  16. Contribution guidelines
  17. Versioning and release policy
  18. Licensing and legal
  19. 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.


Executive summary

The problem

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.

What this project does

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.

Target audience

  • 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?"

Architectural philosophy

  • 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 ci is the local gate; make security-audit is the supply-chain gate.

Key differentiators

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

Feature matrix

Legend: ✅ stable · 🧪 experimental · 📋 planned · ⚠️ platform limitation

Catalog and ingest

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

Globe rendering

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

Observer + visibility

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

Detail, search, and UI

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

Text-only fallback (/text)

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]

Conjunction-replay showcase

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

PWA + offline

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

Stats + dashboards

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

Operations + observability

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

Analytics

Feature Status Notes
Plausible (cookieless) Optional; env var + DNT + localStorage opt-out
Built-in user analytics 📋 Not planned — privacy-by-default

Architecture overview

High-level design

                ┌────────────────────────────────────────────────────────────┐
                │                  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)  │
                └──────────────────────────────────────────────────────────────┘

Service boundaries

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

Data flow

  1. Read path: Browser → Apache/PHP-FPM → Slim front controller → Service container → SQLite → JSON or HTML
  2. Write path (cron): Cron → php bin/console ingest:* → external HTTPS fetch → parse → UPSERT into SQLite
  3. Write path (webhook, optional): External signal → POST /webhooks/ingest/{source} → bearer-auth + rate-limit → proc_open async dispatch to the same ingest CLI

Component relationships

  • 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).

Technology stack

Frontend

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

Backend

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

Database

Component Version Purpose
SQLite 3.40+ (via pdo_sqlite) All persistence — single file at data/sat.db
FTS5 (sqlite built-in) Catalog search

Infrastructure

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

Testing + quality

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

Runtime requirements (verified versions)

  • 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.mjs CLI.
  • SQLite 3.40+ (uses RETURNING clauses).

Installation

Step 1 — Verify prerequisites

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 print

If 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

Step 2 — Clone the repository

git clone git@github.com:CyberSecDef/sat.trackr.live.git
cd sat.trackr.live

Step 3 — Copy environment templates

cp .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.prod

Real .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.

Step 4 — Install dependencies and build the SPA

make install   # composer install + npm install (~30 s)
make build     # vite build → public/build/ (~5 s)

Step 5 — Create the database schema

make migrate   # applies 16 migrations
make migrate-status   # verify: should show all applied, 0 pending

Step 6 — Populate the catalog

make 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)

Step 7 — Verify the installation

make health
# Expect: PHP version + ext: pdo_sqlite + DB connection + migration status +
#         table: satellites with 15,000+ rows, all reporting "ok".

Troubleshooting notes

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

Quick start

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:8000

Expected 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             # → 200

For 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.


Configuration

Environment variables

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.

Sample .env (.env.example verbatim)

# 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=info

See .env.dev.example and .env.prod.example for the overlay-specific keys (DB paths, API credentials).


Usage examples

CLI

# 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

REST API (curl)

# 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]'

UI walkthroughs

Find the ISS and watch it overhead:

  1. Open http://localhost:8000/ (or the LAN URL).
  2. Click 📍 set location in the topbar; allow geolocation (or pick a city).
  3. Press ⌘K (or Ctrl+K); type iss or 25544; press Enter.
  4. The detail panel opens with the ISS selected. Scroll to § Visibility from observer.
  5. The next 5 visible passes appear with rise/peak/set times.
  6. Click the ↑ Sky view topbar toggle to flip into stargazer mode.

Replay a conjunction:

  1. Visit /text/conjunctions to browse the SOCRATES table.
  2. Click any high-probability row; the replay route opens at /conjunction/{primary}/{secondary}.
  3. 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.

Edge cases

  • No WebGL? Visit /text directly. Every meaningful surface has a server-rendered HTML mirror that works without JavaScript.
  • Bad signal on mobile? The PWA service worker caches /text for offline access after a successful first load.
  • /events.atom for RSS readers? Subscribe in your feed reader; ~32 KB cap, refreshed every 10 minutes.

API documentation

Specification

  • 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-dump regenerates public/openapi.json (committed; useful for CI consumers without a running server)

Authentication

  • 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; empty WEBHOOK_SECRET returns 403 on every request before the source allowlist check.

Endpoint inventory

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

Versioning strategy

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.

Request/response shape

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 responses

{
  "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.

Rate limiting

  • 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 to storage/cache/webhook-cooldown.json (atomic tmp+rename writes for PHP-FPM safety).

Webhook payload

# 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.


Security considerations

Threat model

This is a read-only public website with no user accounts and no user-supplied data persisted server-side. The realistic threats are:

  1. Supply-chain compromise — a malicious PHP composer dep or npm package landing during install.
  2. API abuse — high-frequency scraping affecting the SQLite single-writer or the upstream rate budgets.
  3. Webhook abuse — a leaked WEBHOOK_SECRET letting an attacker trigger cron-style ingest commands.
  4. XSS via uncontrolled satellite metadata — an attacker pushing malicious strings into Space-Track / CelesTrak that render unescaped in the SPA.

Authentication and authorization

  • 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).

Encryption

  • 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.

Secrets handling

  • All secrets live in .env.prod on the deploy host, not in the repo.
  • bin/security-audit-dump.sh runs composer audit, npm audit, and queries GitHub's Dependabot / CodeQL / secret-scanning APIs (when gh is authenticated) to surface drift.
  • bin/security-audit-gate.sh (via make security-audit) exits non-zero if any finding is ≥ moderate.
  • make ci chains make security-audit-advisory (non-blocking variant) per the locked Phase 11 rollout policy.

Supply-chain hardening

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

Hardening guidance

  • Apache: ship the project with public/.htaccess enabled — 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.

Known limitations

  • CodeQL does not scan PHP. The bundled .github/workflows/codeql.yml covers 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.

Disclosure policy

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.

Recommended deployment posture

  • Behind HTTPS (Let's Encrypt or Fly LB).
  • WEBHOOK_SECRET set only if push-based ingest is wanted; otherwise leave empty (the default).
  • Cron user is non-root.
  • make security-audit wired into your CI workflow (the Makefile target exists; the GitHub Actions wrapper is a future task).

Deployment

Local

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 SPA

Production

The authoritative deployment guide is docs/deploy.md — walks two production paths end-to-end:

  1. DreamHost VPS (Apache + cron) — the canonical target. Apache vhost config, PHP-FPM selection, document-root pointing, cron entries with MAILTO, log rotation.
  2. Fly.io (single machine + scheduled cron) — sample fly.toml, Dockerfile, .fly.dockerignore, two cron strategies (in-container crond vs scheduled-worker), troubleshooting matrix.

Docker

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-live

Reverse proxy

Apache 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;
}

Cloud deployment

  • 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.

Scaling recommendations

  • 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 is Cache-Control: max-age=N taggable.
  • Horizontal scale-out would require migrating SQLite → Postgres (single-writer becomes the bottleneck under sustained write traffic). Out of scope for the current product.

HA / failover

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).


Operational requirements

Hardware

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)

Storage profile (typical post-ingest)

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

Throughput expectations

  • 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).

Performance benchmarks

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).

Scalability constraints

  • 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_children to RAM.
  • Cesium asset delivery is the realistic bottleneck under load; a CDN in front is the recommended scaling lever.

Logging, monitoring, and telemetry

Log locations and formats

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

Verbosity controls

# .env or .env.{dev,prod}
LOG_LEVEL=info     # debug | info | notice | warning | error | critical

Metrics

No 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).

Health checks

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

Observability integrations

  • Plausible (cookieless) — set PLAUSIBLE_DOMAIN to 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).

SIEM integration

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

Audit logging behavior

  • 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 localStorage and is never sent to the server outside of optional ?lat&lon query parameters on a small subset of endpoints.

Testing

Test strategy

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

Execution

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-advisory

Coverage reporting

Coverage is not currently tracked. PHPUnit + Vitest emit coverage data on --coverage flags; no CI artifact upload is configured.

Mock and staging guidance

  • 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 MockHandler rather than mocking the ingester itself.
  • Playwright runs against the PHP dev server (make serve-style) on localhost:8000. No staging environment is currently provisioned.

CI/CD validation expectations

  • make ci is the local quality gate and what an automated CI should run.
  • make security-audit-advisory is chained into make ci per a 30-day non-blocking rollout policy; future PR will flip to blocking after the rollout window.
  • The bundled .github/workflows/codeql.yml and .github/workflows/sonarqube.yml run static analysis per push.

Flakiness ledger

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.

Performance / load testing

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.


Troubleshooting and FAQ

Globe rendering

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).

Catalog ingest

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.

Service worker

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.

make ci

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.

Diagnostic commands

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 vulnerabilities

Contribution guidelines

Branching strategy

  • main is the default branch and the only long-lived branch.
  • Feature branches: feature/<short-name> or chore/<short-name>.
  • Squash-merge into main. Avoid rebase-and-merge unless the branch has a meaningful per-commit history worth preserving.

Pull request requirements

  • make ci runs 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.

Coding standards

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

Linting and formatting

make lint        # check
make lint-fix    # auto-fix everything fixable

Commit conventions

Free-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.

Review expectations

  • One reviewer minimum on substantial PRs; trivial doc fixes can self-merge.
  • Reviewer runs make ci locally before approving.
  • Authors fix review feedback in fresh commits (squashed at merge), not amends.

Versioning and release policy

Current model

  • Phase-based development. Phases 1-11 are complete (67 chunks total). docs/phase_summary.md is the historical chunk-by-chunk record; docs/phase{1..11}.md preserve the per-phase design intent.
  • No SemVer. package.json ships at 0.0.0; composer.json does not have a version key. The project is hobby-maintenance-mode and doesn't ship versioned releases.

Backward compatibility

  • 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 rollback rolls back the most recent batch (use sparingly).
  • CLI: bin/console subcommand names are stable; flags can be added but not removed without a deprecation cycle.

Breaking change handling

A breaking API change would require:

  1. Land the new behavior at /api/v2/.
  2. Document the migration path in CHANGELOG.md (file doesn't exist yet — a future chunk would create it).
  3. Leave /v1/ running unchanged for at least 90 days post-/v2/ launch.

Deprecation policy

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.

Upgrade procedure (existing deploy)

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                  # verify

Licensing and legal

Software license

AGPL-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.

Third-party licensing

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.

Export restrictions

None known. The project transmits and processes publicly-available orbital data sources (CelesTrak, Space-Track, NOAA). Operators are responsible for their own jurisdictional compliance.

Trademark guidance

"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).

Attribution requirements

  • AGPL distribution — preserve the LICENSE file + 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).

Roadmap and project status

Current maturity

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

Active development

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.

Planned capabilities (no firm timeline)

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

Deprecated components

None currently. All shipped phases remain in production use.

Long-term vision

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.

Maintenance expectations

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.

Support lifecycle

  • Bug reports: open a GitHub issue. Issues marked security get 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.

Acknowledgments

This project would not exist without the public space-data ecosystem built and maintained by:

And to everyone who's ever looked up.

About

Satelite tracking applet

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors