Production-ready real-time satellite tracker powered by live TLE data, SGP4 orbit propagation, and WebSocket streaming
| π° Satellites Tracked | β‘ Refresh Rate | π― SGP4 Accuracy | π Prediction Window | π‘ TLE Sync |
|---|---|---|---|---|
| 14,000+ | 5 seconds | ~2 km (LEO) | 72 hours | Every 30 min |
- β¨ Features
- π Architecture
- π© Tech Stack
- π Quick Start
- π‘ API Reference
- π Database Schema
- π Project Structure
- π³ Docker & Deployment
- β Environment Variables
- πΈ How SGP4 Works
- π License
|
π° Core Tracking
|
π Built for Scale
|
|
π Orbit Prediction
|
π Security
|
OrbitView goes beyond basic position tracking with a full suite of orbital mechanics analysis tools designed for radio operators, conjunction monitoring, and mission planning workflows.
Computes the instantaneous Doppler frequency shift for any satellite relative to a ground observer, enabling accurate radio frequency prediction for uplink/downlink communications.
- Derives range-rate (radial velocity) from the SGP4-propagated ECI velocity vector and the observer's ECEF position
- Supports any nominal carrier frequency β useful for VHF/UHF amateur passes, L-band telemetry, and S-band downlinks
- Returns both the shifted receive frequency and the shift magnitude in Hz/kHz
- Integrates with pass prediction: pre-computes the full Doppler curve across an entire pass so radio operators can configure their SDR or transceiver in advance
| Field | Description |
|---|---|
nominalFreqHz |
Carrier frequency of the satellite transmitter |
dopplerShiftHz |
Instantaneous frequency shift (negative = approaching) |
rangeRateKmSec |
Radial velocity component toward/away from observer |
receivedFreqHz |
Corrected receive frequency accounting for shift |
Detects close approaches between any two tracked satellites within a configurable screening distance, providing early warning of potential collision risk or orbital proximity events.
- Screens all active satellite pairs at configurable time intervals (default: every 15 minutes over a 24-hour window)
- Computes miss distance in km using ECI-frame relative position vectors
- Reports Time of Closest Approach (TCA), miss distance, and relative speed at TCA
- Flags conjunctions below a hard threshold (default: 5 km) as high-risk events
- Streams high-risk conjunction alerts in real time over the
/topic/conjunctionsWebSocket topic - Persists conjunction events to the database for post-event auditing and trend analysis
| Risk Level | Miss Distance | Action |
|---|---|---|
| π’ Nominal | > 25 km | Logged only |
| π‘ Caution | 5 β 25 km | Logged + WebSocket alert |
| π΄ Warning | < 5 km | Logged + WebSocket alert + email notification |
Automatically detects and timestamps discrete orbital events as each satellite propagates forward in time, eliminating the need for clients to poll position endpoints for threshold crossings.
- Apogee / Perigee crossings β detected when the radial distance derivative passes through zero; reports exact crossing time, altitude, and current orbital elements
- Ascending / Descending node crossings β detected when the satellite crosses the equatorial plane (latitude sign change); reports RAAN at crossing for orbit counting and repeat-groundtrack analysis
- Eclipse entry and exit β computed via cylindrical Earth shadow model; reports penumbra entry, umbra entry, umbra exit, and penumbra exit timestamps with fractional eclipse depth
- Events are accumulated per-satellite during propagation sweeps and stored in the
orbital_eventstable for historical queries
Propagation sweep (Tβ β Tβ+72h, 30s steps)
β
ββ Apogee/Perigee detector β orbital_events (type=APOGEE / PERIGEE)
ββ Node crossing detector β orbital_events (type=ASC_NODE / DESC_NODE)
ββ Eclipse shadow model β orbital_events (type=ECLIPSE_ENTRY / ECLIPSE_EXIT)
Combines elevation geometry, solar illumination, and observer sky conditions into a single composite visibility score per pass, replacing the simple isVisible boolean with fine-grained signal quality data.
- Minimum elevation filter (configurable, default 10Β°) eliminates horizon-grazing passes with poor signal-to-noise ratio
- Sunlight status sourced directly from the SGP4 eclipse model β differentiates penumbra from full shadow
- Observer night condition computed from solar depression angle at the ground station
- Atmospheric signal path length estimated from elevation angle for link-budget calculations
- Composite
visibilityClassfield returned on every pass prediction:
visibilityClass |
Conditions |
|---|---|
OPTICAL_AND_RADIO |
Satellite lit + observer night + elevation β₯ 10Β° |
RADIO_ONLY |
Any elevation β₯ 10Β° (regardless of lighting) |
MARGINAL |
Elevation 5Β°β10Β° (atmospheric degradation likely) |
NOT_VISIBLE |
Below horizon or blocked |
Provides a continuous, observer-relative tracking data stream for any registered ground station, going beyond the snapshot AZ/EL values in the /current endpoint.
- Streams real-time azimuth, elevation, and slant range at the WebSocket broadcast cadence (5 seconds)
- Computes antenna pointing rate (Β°/second) to support motorized dish controllers and rotor interfaces
- Calculates two-way light-time delay and one-way propagation delay in milliseconds for timing-sensitive applications
- Supports multiple simultaneous ground stations per user account β each station receives its own
/topic/station/{stationId}WebSocket channel - Ground station profiles stored in the
ground_stationstable with geodetic lat/lon/altitude (WGS-84)
Exposes derived orbital mechanics quantities beyond the basic position/velocity output of the raw SGP4 propagator.
- Relative velocity between satellites β ECI-frame vector subtraction of two propagated velocity states; useful for rendezvous planning and conjunction severity assessment
- Specific orbital energy (vis-viva) β computed as
Ξ΅ = vΒ²/2 β ΞΌ/r; a negative value confirms a bound orbit; magnitude indicates altitude regime - Semi-major axis β derived from mean motion in the TLE via
a = (ΞΌ / nΒ²)^(1/3); reported in km alongside the TLE-native mean motion value - Inclination comparison across a catalog subset β batch endpoint returns inclination distribution for a filtered satellite set, supporting constellation coverage analysis
- Mean motion drift (αΉ) β first derivative of mean motion from TLE Line 1 field; indicates active manoeuvring or significant drag decay
| Analytic | Endpoint | Notes |
|---|---|---|
| Orbital energy | GET /satellites/{id}/analytics |
Returns Ξ΅, a, e, T |
| Relative velocity | GET /satellites/relative?ids=A,B |
ECI ΞV vector + scalar magnitude |
| Inclination distribution | GET /satellites/analytics/inclinations?category=GPS |
Histogram-ready buckets |
| Mean motion drift | Included in /satellites/{id} detail response |
From TLE Line 1 field 7 |
All orbital events and analysis results are broadcast over dedicated STOMP WebSocket topics so frontends and external consumers receive push notifications without polling.
// Subscribe to conjunction warnings across all tracked satellites
client.subscribe('/topic/conjunctions', (msg) => {
const { satelliteA, satelliteB, missDistanceKm, tcaUtc, riskLevel } = JSON.parse(msg.body)
showConjunctionAlert(satelliteA, satelliteB, missDistanceKm, riskLevel)
})
// Subscribe to orbital events for a specific satellite
client.subscribe('/topic/satellite/25544/events', (msg) => {
const { eventType, eventTimeUtc, altitudeKm } = JSON.parse(msg.body)
// eventType: APOGEE | PERIGEE | ASC_NODE | DESC_NODE | ECLIPSE_ENTRY | ECLIPSE_EXIT
logOrbitalEvent(eventType, eventTimeUtc, altitudeKm)
})
// Subscribe to ground station tracking stream
client.subscribe('/topic/station/my-station-id', (msg) => {
const { azimuthDeg, elevationDeg, rangeKm, rangeRateKmSec, pointingRateDegSec } = JSON.parse(msg.body)
updateAntennaDish(azimuthDeg, elevationDeg)
})WebSocket topic reference β advanced channels:
| Topic | Cadence | Payload |
|---|---|---|
/topic/conjunctions |
On detection | Conjunction summary + risk level |
/topic/satellite/{id}/events |
On event crossing | Event type, time, orbital state |
/topic/station/{stationId} |
Every 5 seconds | AZ, EL, range, range-rate, pointing rate |
/topic/satellites/eclipses |
On shadow boundary | Satellite ID, shadow type, entry/exit time |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT LAYER β
β β React 18 + Vite β πΊ Leaflet Map β β‘ STOMP WS β
βββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β HTTPS / WSS
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β GATEWAY LAYER β
β β Nginx β SSL Termination + WS Upgrade β
βββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β HTTP/1.1 + WebSocket
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β APPLICATION LAYER β
β β Spring Boot 3 β π JWT β πΈ SGP4 β π‘ TLE Service β
β π’ STOMP In-Memory Broker β
β π¬ Doppler β π¨ Conjunction β π Events β π Analytics β
ββββββββββββ¬ββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββ
β JPA / JDBC β WebFlux HTTP
ββββββββββββΌβββββββββββ βββββββββββββΌβββββββββββββββββ
β π PostgreSQL 16 β β π CelesTrak API β
β β‘ Caffeine Cache β β (Live TLE Data) β
βββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
Key architecture decisions:
| Decision | Why |
|---|---|
| Stateless JWT | Every backend pod validates tokens independently β zero-config horizontal scaling |
| SGP4 from scratch | All public TLE data is tuned for SGP4 specifically β any other propagator gives wrong results |
| STOMP over raw WebSocket | Built-in pub/sub topics, SockJS fallback, first-class Spring integration |
| Caffeine per-cache TTL | Positions stale in 5s; satellite metadata valid 6h β one global TTL wastes resources |
| ConcurrentHashMap TLE cache | O(1) lookup during propagation avoids DB round-trip on every position request |
| Nginx reverse proxy | Single TLS certificate, WebSocket upgrade, and static file serving in one place |
| Event detection in propagation sweep | Apogee/perigee, node crossings, and eclipse transitions are detected in a single forward sweep rather than separate passes β avoids redundant SGP4 calls and keeps CPU cost O(nΒ·steps) regardless of how many event types are active |
| Conjunction screening via bounding-box pre-filter | Before computing exact miss distances, satellites are binned into spatial cells; only pairs sharing a cell proceed to the full ECI-frame closest-approach calculation β reduces O(nΒ²) pair comparisons by ~95% for typical catalog sizes |
Scaling path: Swap Caffeine β Redis for shared cache across instances. Add a Kafka topic for WebSocket fan-out. Deploy on AWS ECS behind an ALB β no application code changes required.
π₯ Backend β Java / Spring Boot
| Library | Version | Purpose |
|---|---|---|
| Java | 17 LTS | Language β records, sealed classes, text blocks |
| Spring Boot | 3.2.1 | Framework, auto-configuration, embedded Tomcat |
| Spring Security | 6.x | JWT filter chain, method-level @PreAuthorize |
| Spring Data JPA | 3.2.1 | ORM, repository pattern, JPQL queries |
| Spring WebFlux | 3.2.1 | Reactive HTTP client for TLE fetching |
| Spring WebSocket | 3.2.1 | STOMP message broker for live streaming |
| Flyway | 10.x | Versioned SQL schema migrations |
| JJWT | 0.12.3 | JWT generation + validation (HS256) |
| Caffeine | 3.x | High-performance in-process cache |
| HikariCP | 5.x | JDBC connection pool |
| SpringDoc OpenAPI | 2.3.0 | Auto-generated Swagger UI |
| Lombok | 1.18.x | Builders, getters, @Slf4j boilerplate reduction |
β Frontend β React / Vite
| Library | Version | Purpose |
|---|---|---|
| React | 18.2 | UI framework, concurrent rendering |
| Vite | 5.x | Build tool, HMR, code splitting |
| React Leaflet | 4.x | Interactive world map with satellite overlays |
| Zustand | 4.x | Lightweight global state (auth + satellite data) |
| @stomp/stompjs | 7.x | STOMP WebSocket client |
| SockJS-client | 1.6.x | WebSocket transport fallback |
| Recharts | 2.x | Altitude / velocity time-series charts |
| Tailwind CSS | 3.4 | Utility-first dark-theme styling |
| Axios | 1.6 | HTTP client with JWT interceptors + auto-refresh |
| date-fns | 3.x | UTC-safe date formatting |
| Lucide React | 0.303 | Icon system |
π Infrastructure
| Tool | Purpose |
|---|---|
| Docker + Compose | Container orchestration, one-command local setup |
| PostgreSQL 16 | Primary database β JSONB, GIN indexes, TIMESTAMPTZ |
| Nginx | Reverse proxy, SSL termination, WebSocket upgrade |
| Flyway | Schema version control β auto-runs on startup |
| Spring Actuator | /health, /metrics for load balancer probes |
| Maven | Dependency management, multi-stage Docker build |
π³ Docker 24+ and Docker Compose v2 β no local Java or Node required, everything runs in containers.
# 1. Clone the repository
git clone https://github.com/yourusername/satellite-tracker.git
cd satellite-tracker
# 2. Copy environment config and set your secrets
cp .env.example .env
# Edit .env: set JWT_SECRET to $(openssl rand -base64 64)
# 3. Start the full stack
docker compose up --build -d
# 4. Watch startup logs β Flyway runs migrations automatically
docker compose logs -f backend
# 5. Open the app
open http://localhost # React frontend
open http://localhost/swagger-ui.html # Swagger API explorer
β οΈ First boot: fetches live TLE data from CelesTrak (~30 seconds). Falls back to demo TLEs (ISS, Hubble, CSS) if CelesTrak is temporarily unavailable.
Requirements: Java 17+, Node 20+, PostgreSQL 14+
# 1. Create the database
createdb satellite_tracker
psql satellite_tracker -c "CREATE USER satellite_user WITH PASSWORD 'satellite_pass';"
psql satellite_tracker -c "GRANT ALL PRIVILEGES ON DATABASE satellite_tracker TO satellite_user;"
# 2. Start the backend (port 8080)
cd backend
./mvnw spring-boot:run
# 3. Start the frontend (port 3000) β new terminal
cd frontend
npm install
npm run dev
# 4. Verify a live position
curl http://localhost:8080/api/satellites/25544/currentnpm install -g @railway/cli
railway login
railway init
railway upSet these in your Railway dashboard:
DATABASE_URL = postgresql://user:pass@host/db
JWT_SECRET = <openssl rand -base64 64>
CORS_ALLOWED_ORIGINS = https://your-frontend.up.railway.app
SERVER_PORT = 8080
Base URL: http://localhost:8080/api
Swagger UI: http://localhost:8080/swagger-ui.html
| Method | Endpoint | Description |
|---|---|---|
GET |
/satellites |
Paginated catalog. ?query=ISS&category=Weather&activeOnly=true&page=0&size=20 |
GET |
/satellites/featured |
Notable satellites: ISS, Hubble, CSS Tianhe, NOAA, GPS |
GET |
/satellites/{noradId} |
Full detail + current TLE |
GET |
/categories |
All distinct satellite categories |
| Method | Endpoint | Description |
|---|---|---|
GET |
/satellites/{noradId}/current |
Real-time position. Add ?lat=51.5&lon=-0.12 for AZ/EL/range |
GET |
/satellites/{noradId}/predict |
Future position. ?minutes=90 (max 4320 = 72h) |
GET |
/satellites/{noradId}/track |
Ground track polyline. ?start=&end=&intervalSeconds=30 |
GET |
/satellites/{noradId}/passes |
Pass predictions. ?lat=51.5&lon=-0.12&hours=24&minElevation=10 |
GET |
/satellites/positions/all |
All satellite positions for map overview. ?limit=100 |
π¦ Example: Current position response
{
"noradCatalogId": 25544,
"name": "ISS (ZARYA)",
"timestamp": "2024-01-15T14:32:07.123Z",
"latitude": 51.6234,
"longitude": -12.4521,
"altitudeKm": 418.3,
"velocityKmPerSec": 7.66,
"azimuth": 234.1,
"elevation": 42.7,
"rangeKm": 612.4,
"isDaylight": true,
"isVisible": true
}π¦ Example: Pass prediction response
{
"noradCatalogId": 25544,
"name": "ISS (ZARYA)",
"riseTime": "2024-01-15T19:42:00Z",
"maxElevationTime": "2024-01-15T19:45:30Z",
"setTime": "2024-01-15T19:49:00Z",
"maxElevationDeg": 68.4,
"riseAzimuthDeg": 311.2,
"setAzimuthDeg": 127.8,
"durationSeconds": 420,
"isVisualPass": true,
"isDaylightPass": true,
"isObserverNight": true
}| Method | Endpoint | Description |
|---|---|---|
GET |
/satellites/{noradId}/doppler |
Doppler shift at current time. ?lat=&lon=&freqHz=437550000 |
GET |
/satellites/{noradId}/passes/doppler |
Full Doppler curve across next pass. ?lat=&lon=&freqHz= |
GET |
/satellites/{noradId}/analytics |
Orbital energy, semi-major axis, mean motion drift |
GET |
/satellites/{noradId}/events |
Historical orbital events (apogee, node crossings, eclipses). ?hours=24 |
GET |
/satellites/relative |
Relative velocity between two satellites. ?ids=25544,20580 |
GET |
/satellites/analytics/inclinations |
Inclination distribution for a satellite category. ?category=GPS |
GET |
/conjunctions |
Active conjunction warnings. ?minRiskLevel=CAUTION&hours=24 |
GET |
/conjunctions/{id} |
Detailed conjunction report with TCA, miss distance, and relative velocity |
GET |
/ground-stations |
List registered ground stations for the authenticated user |
POST |
/ground-stations |
Register a new ground station. Body: {name, lat, lon, altitudeM} |
DELETE |
/ground-stations/{stationId} |
Remove a ground station |
π¦ Example: Doppler shift response
{
"noradCatalogId": 25544,
"name": "ISS (ZARYA)",
"timestamp": "2024-01-15T14:32:07.123Z",
"nominalFreqHz": 437550000,
"dopplerShiftHz": -3241,
"receivedFreqHz": 437546759,
"rangeRateKmSec": -2.228,
"rangeKm": 612.4,
"elevationDeg": 42.7
}π¦ Example: Conjunction warning response
{
"conjunctionId": "conj-20240115-001",
"satelliteA": { "noradId": 25544, "name": "ISS (ZARYA)" },
"satelliteB": { "noradId": 43205, "name": "COSMOS 2519" },
"timeOfClosestApproach": "2024-01-15T22:17:43Z",
"missDistanceKm": 3.82,
"relativeVelocityKmSec": 14.3,
"riskLevel": "WARNING",
"detectedAt": "2024-01-15T14:00:00Z"
}| Method | Endpoint | Description |
|---|---|---|
POST |
/auth/register |
Register. Body: {username, email, password, displayName} |
POST |
/auth/login |
Login. Body: {usernameOrEmail, password} β returns JWT tokens |
POST |
/auth/refresh |
Refresh tokens. Body: {refreshToken} |
GET |
/auth/me |
Current user info (requires Authorization: Bearer <token>) |
PUT |
/auth/preferences |
Update observer location, timezone, theme |
POST |
/auth/favorites/{noradId} |
Add satellite to favorites |
DELETE |
/auth/favorites/{noradId} |
Remove satellite from favorites |
Connect to ws://localhost:8080/ws with SockJS fallback:
import { Client } from '@stomp/stompjs'
import SockJS from 'sockjs-client'
const client = new Client({
webSocketFactory: () => new SockJS('http://localhost:8080/ws'),
onConnect: () => {
// All satellite positions β server broadcasts every 5 seconds
client.subscribe('/topic/satellites/all', (msg) => {
const { positions, satelliteCount, timestamp } = JSON.parse(msg.body)
updateMap(positions) // positions: [{noradId, lat, lon, alt, ...}]
})
// Track one specific satellite
client.subscribe('/topic/satellite/25544', handleISSUpdate)
client.publish({
destination: '/app/track',
body: JSON.stringify({ noradId: 25544 })
})
}
})
client.activate()satellites ββ< tle_records
β
βββ< tracking_logs >ββ users
β β
βββ< orbital_events user_roles
user_favorite_satellites
ground_stations
π Full schema β key tables
-- Satellite master catalog
CREATE TABLE satellites (
id BIGSERIAL PRIMARY KEY,
norad_catalog_id INTEGER NOT NULL UNIQUE, -- ISS=25544, Hubble=20580
cospar_id VARCHAR(20), -- e.g. "1998-067A"
name VARCHAR(100) NOT NULL,
category VARCHAR(100),
orbital_period_minutes DOUBLE PRECISION,
inclination_deg DOUBLE PRECISION,
apogee_km DOUBLE PRECISION,
perigee_km DOUBLE PRECISION,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- GIN index: fast full-text satellite name search
CREATE INDEX idx_satellite_name ON satellites
USING gin(to_tsvector('english', name));
-- TLE history β only one is_current=true per satellite at any time
CREATE TABLE tle_records (
id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL REFERENCES satellites(id),
line1 VARCHAR(70) NOT NULL, -- NORAD TLE Line 1 (69 chars)
line2 VARCHAR(70) NOT NULL, -- NORAD TLE Line 2 (69 chars)
epoch TIMESTAMPTZ NOT NULL,
is_current BOOLEAN NOT NULL DEFAULT false,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Partial index: instant current-TLE lookup without full scan
CREATE INDEX idx_tle_satellite_current
ON tle_records(satellite_id, is_current)
WHERE is_current = true;
-- Users table
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
observer_latitude DOUBLE PRECISION,
observer_longitude DOUBLE PRECISION,
observer_altitude_m INTEGER DEFAULT 0,
timezone VARCHAR(50) DEFAULT 'UTC',
theme VARCHAR(20) DEFAULT 'dark',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login_at TIMESTAMPTZ
);
-- Ground stations β per-user observer profiles for tracking and Doppler
CREATE TABLE ground_stations (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
latitude DOUBLE PRECISION NOT NULL,
longitude DOUBLE PRECISION NOT NULL,
altitude_m INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Orbital events β apogee/perigee crossings, node crossings, eclipse transitions
CREATE TABLE orbital_events (
id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL REFERENCES satellites(id),
event_type VARCHAR(30) NOT NULL, -- APOGEE | PERIGEE | ASC_NODE | DESC_NODE | ECLIPSE_ENTRY | ECLIPSE_EXIT
event_time TIMESTAMPTZ NOT NULL,
altitude_km DOUBLE PRECISION,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
extra_data JSONB, -- eclipse depth, RAAN at node, etc.
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orbital_events_satellite_time
ON orbital_events(satellite_id, event_time DESC);
-- Conjunction warnings β close approach records between satellite pairs
CREATE TABLE conjunctions (
id BIGSERIAL PRIMARY KEY,
satellite_a_id BIGINT NOT NULL REFERENCES satellites(id),
satellite_b_id BIGINT NOT NULL REFERENCES satellites(id),
time_of_closest_approach TIMESTAMPTZ NOT NULL,
miss_distance_km DOUBLE PRECISION NOT NULL,
relative_velocity_km_sec DOUBLE PRECISION,
risk_level VARCHAR(20) NOT NULL, -- NOMINAL | CAUTION | WARNING
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMPTZ
);
CREATE INDEX idx_conjunctions_tca ON conjunctions(time_of_closest_approach DESC);
CREATE INDEX idx_conjunctions_risk ON conjunctions(risk_level) WHERE resolved_at IS NULL;Schema design decisions:
| Decision | Rationale |
|---|---|
| NORAD ID β DB PK | NORAD ID is the universal external identifier; internal BIGSERIAL PK stays stable |
Orbital elements on satellites |
Denormalized for fast catalog queries without a join on every request |
is_current partial index |
Tiny index vs full table scan β only one row per satellite matches |
TIMESTAMPTZ everywhere |
Forces UTC storage; eliminates daylight-saving bugs in pass prediction |
| GIN index on satellite name | Supports LIKE '%query%' searches efficiently |
orbital_events JSONB extra_data |
Stores event-type-specific fields (eclipse depth, RAAN) without schema churn |
conjunctions unresolved partial index |
Keeps the active-warning query fast without scanning historical resolved rows |
Flyway migrations live in
backend/src/main/resources/db/migration/. AddV2__description.sqlfiles for future changes β they run automatically on startup.
β Backend β Java package layout
backend/src/main/java/com/satellitetracker/
β
βββ SatelliteTrackerApplication.java β @SpringBootApplication entry point
β
βββ config/
β βββ SecurityConfig.java β JWT filter chain, CORS, endpoint rules
β βββ WebSocketConfig.java β STOMP broker, SockJS endpoint
β βββ CacheConfig.java β Caffeine per-cache TTL policies
β βββ AppConfig.java β WebClient bean for CelesTrak requests
β
βββ controller/
β βββ SatelliteController.java β REST /api/satellites/**
β βββ AuthController.java β REST /api/auth/**
β βββ ConjunctionController.java β REST /api/conjunctions/**
β βββ GroundStationController.java β REST /api/ground-stations/**
β βββ SatelliteWebSocketController.java β Scheduled broadcast + @MessageMapping
β
βββ service/
β βββ TleService.java β CelesTrak fetch, parse, cache, schedule
β βββ OrbitPropagationService.java β SGP4 positions, tracks, pass prediction
β βββ OrbitalEventService.java β Apogee/perigee, node, eclipse detection
β βββ ConjunctionService.java β Pairwise close-approach screening + alerts
β βββ DopplerService.java β Range-rate and frequency shift computation
β βββ GroundStationService.java β Observer tracking, pointing rate, link delay
β βββ OrbitalAnalyticsService.java β Energy, semi-major axis, relative velocity
β βββ SatelliteService.java β Catalog search, featured, categories
β βββ UserService.java β Register, login, JWT, preferences
β
βββ model/
β βββ entity/
β β βββ Satellite.java β @Entity: satellites table
β β βββ TleRecord.java β @Entity: tle_records table
β β βββ User.java β @Entity: users table
β β βββ GroundStation.java β @Entity: ground_stations table
β β βββ OrbitalEvent.java β @Entity: orbital_events table
β β βββ Conjunction.java β @Entity: conjunctions table
β β βββ TrackingLog.java β @Entity: tracking_logs table
β βββ dto/
β βββ SatelliteDto.java β Summary, Detail, TleDto
β βββ OrbitDto.java β Position, OrbitTrack, PassPrediction
β βββ DopplerDto.java β DopplerShift, DopplerCurve
β βββ ConjunctionDto.java β ConjunctionSummary, ConjunctionDetail
β βββ OrbitalAnalyticsDto.java β Energy, SemiMajorAxis, RelativeVelocity
β βββ AuthDto.java β LoginRequest, RegisterRequest, AuthResponse
β
βββ repository/
β βββ SatelliteRepository.java β JPA: fulltext search, category filter
β βββ TleRecordRepository.java β JPA: current TLE, markAllNotCurrent
β βββ UserRepository.java β JPA: findByUsernameOrEmail
β βββ GroundStationRepository.java β JPA: findByUserId
β βββ OrbitalEventRepository.java β JPA: findBySatelliteAndEventTimeBetween
β βββ ConjunctionRepository.java β JPA: active warnings, risk level filter
β βββ TrackingLogRepository.java
β
βββ security/
β βββ JwtUtils.java β HS256 token generation + validation
β βββ JwtAuthenticationFilter.java β OncePerRequestFilter: extract & validate JWT
β βββ JwtAuthenticationEntryPoint.java β 401 JSON error response
β βββ UserDetailsServiceImpl.java β Spring Security UserDetails bridge
β
βββ util/
β βββ Sgp4Propagator.java β β
Complete SGP4 algorithm (Vallado 2006, ~750 LOC)
β βββ TleParser.java β 2/3-line TLE format parser + checksum validation
β βββ AstroUtils.java β Sun position, AZ/EL/range, GMST, ECEF conversion
β βββ DopplerUtils.java β Range-rate projection, relativistic correction
β
βββ exception/
βββ GlobalExceptionHandler.java β @RestControllerAdvice, RFC 7807 error format
βββ ResourceNotFoundException.java β 404
βββ TleParseException.java β 422
βββ ConflictException.java β 409
β Frontend β React source layout
frontend/src/
β
βββ main.jsx β ReactDOM.createRoot, BrowserRouter
βββ App.jsx β Routes, WebSocket init, global data fetch
βββ index.css β Tailwind directives + Leaflet dark overrides
β
βββ pages/
β βββ MapPage.jsx β World map + live satellite overlay + sidebar
β βββ SatellitesPage.jsx β Searchable paginated satellite catalog
β βββ SatelliteDetailPage.jsx β Detail panel + orbit chart + pass predictions
β βββ PassesPage.jsx β Observer pass predictor with location input
β βββ ConjunctionsPage.jsx β Live conjunction warning dashboard
β βββ AnalyticsPage.jsx β Orbital analytics: energy, inclination, Doppler
β βββ LoginPage.jsx β JWT login / registration forms
β
βββ components/
β βββ map/
β β βββ SatelliteMap.jsx β Leaflet MapContainer with dark tile layer
β β βββ SatelliteMarker.jsx β Animated divIcon + info popup
β β βββ OrbitPath.jsx β Ground track Polyline, Β±180Β° date-line split
β βββ satellite/
β β βββ SatellitePanel.jsx β Sidebar: live position stats + controls
β β βββ SatelliteCard.jsx β Catalog grid card with orbital parameters
β β βββ PassTable.jsx β Upcoming passes with rise/set/max-elevation
β β βββ DopplerChart.jsx β Recharts Doppler curve across pass window
β β βββ OrbitalEventLog.jsx β Timestamped apogee/node/eclipse event feed
β β βββ OrbitalChart.jsx β Recharts altitude + velocity over time
β βββ conjunction/
β β βββ ConjunctionAlert.jsx β Toast notification for WARNING-level events
β β βββ ConjunctionTable.jsx β Active close-approach table with risk badges
β βββ groundstation/
β β βββ StationManager.jsx β CRUD UI for ground station profiles
β β βββ StationTracker.jsx β Live AZ/EL/range display + pointing rate
β βββ layout/
β β βββ Layout.jsx β Top nav bar + live/offline status indicator
β βββ ui/
β βββ TimeSlider.jsx β Β±72h time scrubber for position prediction
β βββ SearchBar.jsx β Debounced satellite search input
β βββ LoadingSpinner.jsx
β
βββ store/
β βββ index.js β Zustand: useAuthStore + useSatelliteStore + useConjunctionStore
β
βββ services/
β βββ api.js β Axios instance, JWT interceptors, auto-refresh
β βββ websocket.js β STOMP client: connect, subscribe, reconnect
β
βββ utils/
βββ formatters.js β Coordinate, speed, time, magnitude, frequency formatters
π Monorepo root
satellite-tracker/
βββ satellite-animation.svg β Animated orbital diagram (this header)
βββ docker-compose.yml β Full stack: postgres + backend + frontend + nginx
βββ docker-compose.prod.yml β Production overrides (resource limits, restart policy)
βββ .env.example β Environment variable template
βββ README.md
βββ LICENSE
β
βββ backend/
β βββ Dockerfile β Multi-stage: Maven build β JRE 17 slim runtime
β βββ pom.xml
β βββ src/
β βββ main/java/ β Application source
β βββ main/resources/
β β βββ application.properties
β β βββ db/migration/
β β βββ V1__initial_schema.sql
β β βββ V2__advanced_orbital_tables.sql β ground_stations, orbital_events, conjunctions
β βββ test/
β
βββ frontend/
β βββ Dockerfile β Multi-stage: Node build β Nginx static serve
β βββ vite.config.js
β βββ tailwind.config.js
β βββ src/
β
βββ nginx/
β βββ nginx.conf β Reverse proxy + WebSocket upgrade headers
β
βββ database/
βββ init.sql β DB user creation + initial permissions
π docker-compose.yml β full stack
version: '3.9'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: satellite_tracker
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME}"]
interval: 10s
retries: 5
networks: [satellite-net]
backend:
build: ./backend
environment:
DATABASE_URL: jdbc:postgresql://postgres:5432/satellite_tracker
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
CORS_ALLOWED_ORIGINS: http://localhost
depends_on:
postgres: { condition: service_healthy }
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
retries: 3
networks: [satellite-net]
frontend:
build:
context: ./frontend
args:
VITE_API_URL: "" # empty = relative URLs proxied through nginx
networks: [satellite-net]
nginx:
image: nginx:alpine
ports: ["80:80", "443:443"]
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on: [backend, frontend]
networks: [satellite-net]
volumes:
pgdata:
networks:
satellite-net:π Backend Dockerfile β multi-stage build
# Stage 1: Build with full JDK + Maven
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn -f pom.xml clean package -DskipTests
# Stage 2: Minimal JRE runtime (~180MB vs ~420MB JDK)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --start-period=60s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]β nginx.conf β reverse proxy + WebSocket upgrade
upstream backend { server backend:8080; }
upstream frontend { server frontend:80; }
server {
listen 80;
# React SPA β serve index.html for all routes
location / {
proxy_pass http://frontend;
try_files $uri $uri/ /index.html;
}
# Spring Boot REST API
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# STOMP WebSocket (SockJS + raw WS)
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# Swagger UI
location /swagger-ui/ {
proxy_pass http://backend/swagger-ui/;
}
}| Step | Action |
|---|---|
| 1. Database | Provision managed Postgres (Railway, Render, AWS RDS Free Tier, or Supabase) |
| 2. Secrets | openssl rand -base64 64 for JWT_SECRET β store in platform secrets vault |
| 3. Backend | Deploy backend/Dockerfile β Flyway runs V1__initial_schema.sql on first boot |
| 4. Frontend | Deploy frontend/Dockerfile with VITE_API_URL=https://your-backend.com as build arg |
| 5. Verify | GET /actuator/health β {"status":"UP","components":{"db":{"status":"UP"}}} |
Copy .env.example β .env and fill in your values:
# ββ Database βββββββββββββββββββββββββββββββββββββββββββββββββ
DATABASE_URL=jdbc:postgresql://localhost:5432/satellite_tracker
DATABASE_USERNAME=satellite_user
DATABASE_PASSWORD=changeme_in_production
DB_POOL_SIZE=20
# ββ JWT β generate before deploying! βββββββββββββββββββββββββ
# Run: openssl rand -base64 64
JWT_SECRET=your-256-bit-base64-encoded-secret-here
JWT_EXPIRATION_MS=86400000 # 24 hours
JWT_REFRESH_EXPIRATION_MS=604800000 # 7 days
# ββ CORS βββββββββββββββββββββββββββββββββββββββββββββββββββββ
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://yoursite.com
# ββ TLE Data βββββββββββββββββββββββββββββββββββββββββββββββββ
TLE_REFRESH_INTERVAL=30 # Minutes between CelesTrak syncs
# ββ Rate Limiting βββββββββββββββββββββββββββββββββββββββββββββ
RATE_LIMIT_RPM=60
RATE_LIMIT_BURST=10
# ββ Advanced Orbital Analysis ββββββββββββββββββββββββββββββββ
CONJUNCTION_SCREEN_INTERVAL_MIN=15 # Minutes between conjunction screening sweeps
CONJUNCTION_WARNING_THRESHOLD_KM=5 # Miss distance threshold for WARNING alerts
CONJUNCTION_CAUTION_THRESHOLD_KM=25 # Miss distance threshold for CAUTION alerts
EVENT_DETECTION_STEP_SECONDS=30 # Propagation step size for orbital event sweeps
# ββ Server βββββββββββββββββββββββββββββββββββββββββββββββββββ
SERVER_PORT=8080
SHOW_SQL=false # Set true for SQL query debuggingπ΄ Security checklist before going live:
- Change
JWT_SECRETfrom the placeholder β useopenssl rand -base64 64- Change
DATABASE_PASSWORDfromchangeme- Remove or change the seeded admin account (default:
admin/Admin@123!)- Restrict
CORS_ALLOWED_ORIGINSto your exact production frontend URL- Enable HTTPS on your load balancer or via Let's Encrypt / Certbot
The core of this platform is a complete from-scratch SGP4 implementation in Java (Sgp4Propagator.java, ~750 lines), based on Vallado et al. "Revisiting Spacetrack Report #3", AIAA 2006-6753.
Why SGP4 specifically? All public TLE data is deliberately tuned to the SGP4 force model. TLE elements are mean elements that absorb modelling errors as corrections β using any other propagator (Cowell, Runge-Kutta, etc.) with TLE data produces incorrect positions because the data and the algorithm are mathematically coupled.
Propagation pipeline:
TLE (line1 + line2)
β
βΌ
Parse mean orbital elements
(inclination, RAAN, eccentricity, arg of perigee, mean anomaly, mean motion, B*)
β
βΌ
SGP4 Initialization β compute secular drift rates due to:
β’ J2, J3, J4 Earth oblateness perturbations
β’ Atmospheric drag (B* drag term)
β’ Resonance classification: LEO vs deep-space
β
βΌ
Propagate to target time (minutesFromEpoch):
β’ Apply secular drift (mean motion, nodal regression, perigee precession)
β’ Solve Kepler's equation via Newton-Raphson iteration
β’ Apply short-period J2 corrections
β
βΌ
ECI state vector: [x, y, z km] + [vx, vy, vz km/s]
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
Rotate ECI β ECEF via GMST Advanced analysis layer
Apply Bowring iterative method β’ Doppler: project ECI velocity onto
β Geodetic lat/lon/alt (WGS-84) observer LOS vector β range-rate β
Ξf = fβ Β· (rangeRate / c)
β’ Eclipse: cylindrical shadow model
β penumbra / umbra depth scalar
β’ Conjunction: ECI-frame ΞR between
two propagated state vectors
β’ Orbital energy: Ξ΅ = vΒ²/2 β ΞΌ/r
Usage example:
// Initialize from raw TLE strings
Sgp4Propagator sgp4 = new Sgp4Propagator(line1, line2);
// Minutes since TLE epoch (negative = past, positive = future)
double jd = AstroUtils.toJulianDate(Instant.now());
double minutesFromEpoch = (jd - sgp4.getEpochJulianDate()) * 1440.0;
// Propagate β ECI state vector
EciState eci = sgp4.propagate(minutesFromEpoch);
// Convert to geodetic coordinates (WGS-84)
GeodeticPosition geo = Sgp4Propagator.eciToGeodetic(eci.positionArray(), jd);
System.out.printf("ISS: %.4fΒ°N %.4fΒ°E %.1f km alt %.2f km/s%n",
geo.latitude(), geo.longitude(), geo.altitudeKm(), eci.speed());
// ISS: 51.6234Β°N -12.4521Β°E 418.3 km alt 7.66 km/s
// Doppler shift for a 437.550 MHz uplink from a ground observer
double dopplerHz = DopplerUtils.computeShiftHz(eci, observerEcef, 437_550_000.0);
System.out.printf("Doppler shift: %+.0f Hz β receive on %.3f MHz%n",
dopplerHz, (437_550_000.0 + dopplerHz) / 1e6);
// Doppler shift: -3241 Hz β receive on 437.547 MHzAccuracy by TLE age:
| TLE Age | Expected Position Error |
|---|---|
| < 24 hours | ~1β3 km |
| 1β3 days | ~5β15 km |
| 7 days | ~10β30 km |
| > 30 days |
This project is licensed under the MIT License β see the LICENSE file for details.
MIT License β free to use, modify, and distribute with attribution.
Built with β Java + β React
TLE data from CelesTrak Β· SGP4 algorithm by Vallado et al. (2006)
Give β Star this repo if you found it useful