<- Back to work

apsis

Shipped

Satellite pass prediction on real orbital data - open source.

apsis is a small open-source backend that answers one operational question: when will a given satellite be visible from a given ground station, and where will it pass overhead? It pulls real two-line elements (TLEs) from CelesTrak, propagates them with SGP4 through skyfield, and returns the upcoming visible passes together with the sub-satellite ground track as GeoJSON. It runs on FastAPI and PostgreSQL/PostGIS, with scheduled jobs and a transactional outbox built into Postgres itself - no message broker.

Real ISS (NORAD 25544) passes over two ESA ground stations in Spain - Cebreros and Maspalomas - computed by apsis from real orbital data. Cyan lines are the sub-satellite ground tracks; rings mark culmination. Click a track for the pass details.Static fixture computed by apsis. Basemap: Natural Earth (public domain).

The problem: ground-segment visibility

A ground station can only talk to a satellite while it is above the local horizon. Planning that contact window means propagating the orbit forward, finding the rise / culminate / set events above a minimum elevation, and projecting the path onto the ground. This is the same family of problem I work on in the European ground segment - satellite ground-segment tooling at Telespazio around SIBA and Sentinel/Copernicus. apsis distills it into a clean 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). The reflex answer is a message broker plus a worker framework. apsis uses PostgreSQL for both: a scheduled_jobs table with a lease for the recurring side, and a transactional outbox (LISTEN/NOTIFY plus SELECT ... FOR UPDATE SKIP LOCKED) so an event fires if and only if the database write committed. One fewer moving part to deploy, secure and monitor.

The geometry: PostGIS

Ground stations are points, ground tracks are linestrings, all in WGS84 (longitude/latitude, SRID 4326). PostGIS stores and indexes them; the API serialises each track straight to GeoJSON - which is exactly what the map above renders, unmodified. Asking which passes cross a bounding box is then a GiST index away.

When I would not do this

The honest part. A Postgres-native queue is the right call for a single service that already owns its database and needs reliable, transactional background work at human scale. It is the wrong call at very high throughput, for fan-out to many independent consumers, or for cross-language and cross-service integration - reach for a real broker there. 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, offloaded from the async event loop with a small run_blocking helper. The design decisions are recorded as ADRs in the repository.