Satellite orbit visualization and pass prediction.
Note
The next branch contains many improvements from a bigger refactoring and is the recommended branch currently.
This version is currently deployed to satvis.space.
Planning to merge this back to the master main in the next few months.
- Calculate position and orbit of satellites from CelesTrak GP element sets (OMM/TLE)
- Set groundstation through geolocation or pick on map
- Calculate passes for a set groundstation
- Local browser notifications for passes
- Serverless architecture
- Works offline as Progressive Web App (PWA)
Initialize submodules and install build dependencies:
git submodule update --init
mise trust && mise install # toolchain (Node 24, pnpm 11, prek; see mise.toml)
mise setup # install the pre-commit hooks
pnpm install
A single pnpm install at the repository root installs dependencies for both
the SPA and the worker/ package.
pnpm devfor the dev server (proxies/apito https://satvis.space, so satellite data works without a local worker)pnpm dev:hostto expose the dev server on the local networkpnpm buildto build the application (output indistfolder)pnpm previewto preview the production build locallypnpm update-gpto refresh the static satellite-data snapshot (see below)
To run the frontend against a local worker instead of the deployed API:
pnpm dev:worker # wrangler dev on :8080
SATVIS_API_PROXY=http://localhost:8080 pnpm dev # frontend proxies /api → local worker
The worker's cron trigger fills Workers KV. To run it once locally (wrangler
dev is started with --test-scheduled), hit the scheduled endpoint:
curl "http://localhost:8080/__scheduled?cron=23+*%2F3+*+*+*"
Then GET /api/groups.json lists the refreshed groups and
GET /api/gp/starlink.json returns an OMM element-set array.
Element sets come from CelesTrak as OMM JSON
(CelesTrak is phasing out TLE for new objects). The Cloudflare Worker in
worker/ fetches and serves them:
- A cron trigger (every 3 h) refreshes each group into Workers KV; failed sources keep the last-known-good copy.
GET /api/gp/<group>.json— one group's element sets (OMM array, with per-satellite metadata attached; see below).GET /api/groups.json— the group index (also the frontend's worker probe).
Configuration is declarative YAML, not shell scripts. Each config file
contributes two independent sections: groups (what is served, as which unit) and
satellites (static per-satellite facts, keyed by NORAD id).
-
The core config lives in
worker/src/config/satvis.core.yaml(CelesTrak pass-throughs, plus the satellite table). -
Plugins add
data/custom/<plugin>/satvis.yamlwithsources/select/rename/include/extraRecordsFile. Example (data/custom/example/satvis.yaml):groups: - name: iss sources: [{ celestrak: stations }] satellites: - { noradId: 25544, upstreamName: ISS (ZARYA), name: ISS }
pnpm --filter satvis-worker generate-groups merges the core config with every
data/custom/*/satvis.yaml (inlining extraRecordsFile element sets) into the
gitignored worker/src/config/satvis.generated.json used by the worker.
For plain static hosting (or forks without a worker), run
pnpm update-gp before pnpm build. It runs the same refresh pipeline as the
cron — including metadata enrichment — and writes a static snapshot into
data/gp/ (<group>.json, index.json; gitignored). At runtime the app probes
/api/groups.json; if that fails it falls back to the static data/gp/
snapshot, so all presets keep working without the worker.
Static per-satellite facts — per-side swath extents, sensor cone FOV, model URL,
operator — live in the satellites table of satvis.core.yaml (and of any plugin
config), keyed by NORAD id. The refresh attaches each matching satellite's facts to
its served record under a lowercase metadata key, so metadata travels with the
element set instead of being matched against a separate rule list in the browser.
Satellites absent from the table carry no metadata and fall back to the defaults in
src/config/satelliteMetadata.ts.
Swath extents are per-side cross-track distances from the ground track,
relative to flight direction — not halves of a total width, because a tilted sensor
reaches further one way than the other. See
docs/adr/0002-static-satellite-metadata.md.
To provide pass notifications on iOS where local browser notifications are not supported a simple app wraps the webview and handles the scheduling of UserNotifications.
This project is licensed under the MIT License - see LICENSE file for details.
Inspired by a visualization developed for the MOVE-II CubeSat project by Jonathan, Marco and Flo.
