apsis
ShippedOpen-source satellite pass prediction on real orbital data.
apsis is a small open-source backend that answers one operational question: when is the next contact window between a ground station and a satellite, and where does the satellite cross the sky? It ingests two-line elements (TLEs) from CelesTrak, propagates them with SGP4 through skyfield, and returns every pass above the elevation mask of the station for the next 48 hours, each with its sub-satellite ground track as GeoJSON. It runs on FastAPI and PostgreSQL/PostGIS, with scheduled jobs and a transactional outbox built into Postgres itself. There is no message broker anywhere in the system.
Interactive map (requires JavaScript). The passes are described in the text.
Pass schedule (UTC)
| Date | AOS-LOS | Duration | Max elev | Station |
|---|
The problem: ground-segment visibility
A ground station can only talk to a satellite while it sits above the local horizon, and in practice above an elevation mask, because low-elevation signals cross too much atmosphere and terrain. Planning a contact window means propagating the orbit forward, finding when the satellite climbs past the mask (AOS), where it culminates, and when it drops back below (LOS), then projecting the path onto the ground. It is the same family of problem I work on at Telespazio around SIBA and Sentinel/Copernicus. apsis distills it into a public reference with none of the confidential parts.
The thesis: no broker
apsis has two kinds of background work: recurring (refresh TLEs every couple of hours) and reactive (when an orbit changes, recompute its passes). Instead of adding a message broker plus a worker framework, apsis runs both on PostgreSQL: a scheduled_jobs table with a lease for the recurring side, and a transactional outbox (LISTEN/NOTIFY plus SELECT ... FOR UPDATE SKIP LOCKED) that only fires an event once the database write has committed. The dual-write problem disappears because there is no second system to write to.
The geometry: PostGIS
Ground stations are points and ground tracks are linestrings, everything in WGS84 (longitude/latitude, SRID 4326). PostGIS stores and indexes the geometry, and the API serialises each track straight to GeoJSON, which is exactly what the map above renders. Spatial questions (which passes cross this bounding box, which stations sit inside this area) run on a GiST index instead of application code.
When I would not do this
A Postgres-native queue fits a single service that already owns its database and needs reliable, transactional background work at human scale. It stops fitting at very high throughput, with fan-out to many independent consumers, or across languages and services: those are broker problems, and pretending otherwise is how this pattern gets a bad name. The full trade-off is written up as a note.
Architecture
Layered per context: API to use cases to services and repositories to models. The compute core (orbit propagation, pass finding, geometry) is pure, synchronous and side-effect free, and a small run_blocking helper keeps it off the async event loop. Every non-obvious decision is recorded as an ADR in the repository.