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+*%2F6+*+*+*"
Then GET /api/groups.json lists the refreshed groups and
GET /api/gp/starlink.json returns an OMM element-set array.
POST /api/refresh runs the same refresh on demand and reports per-source
diagnostics. It needs a bearer token, since one run pulls ~7 MB from CelesTrak
against a 250 MB/day per-IP cap:
curl -X POST -H "Authorization: Bearer $REFRESH_TOKEN" http://localhost:8080/api/refresh
Locally the token comes from worker/.dev.vars (copy worker/.dev.vars.example);
deployed it is a Worker secret, set with wrangler secret put REFRESH_TOKEN.
With no secret set the endpoint returns 503 rather than running unauthenticated.
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 6 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.
