API architecture
What this page is
The shape of the HTTP interface: 46 endpoints over an in-memory store, a three-code error model, and a set of hard limits that decide which measurements can be made here and which cannot.
State is a single in-memory store
type AppState = Arc<Mutex<Store>>;
struct Store {
constellations: HashMap<String, Constellation>,
scenarios: HashMap<String, StoredScenario>,
}Two consequences follow directly, and both matter operationally.
State is process-local and volatile. There is no database. Constellations
and scenarios live in a HashMap for the lifetime of the process.
Restarting the service discards everything. Running two replicas behind a load
balancer gives each its own store, so a constellation created through one is
absent from the other and requests fail with not_found.
The service is therefore a single-instance analysis backend, not a horizontally scalable one. Treating it otherwise produces failures that look intermittent and are not.
The lock is not held during computation
The obvious worry with one mutex over all state is that a long analysis blocks every other request. It does not, because handlers take the lock only to clone what they need:
let constellation = {
let store = state.lock().expect("store mutex poisoned");
store
.constellations
.get(&req.constellation_id)
.cloned()
.ok_or_else(|| ApiError::not_found(/* ... */))?
};
// lock released here; the expensive work happens afterCloning a constellation costs memory proportional to the satellite count and buys concurrent analysis. For a 5000-satellite ceiling that is a sound trade.
Errors are three codes and a message
struct ApiError {
status: StatusCode,
code: &'static str,
message: String,
}Rendered as:
{ "error": { "code": "invalid_request", "message": "..." } }The entire vocabulary is three codes:
| Code | Status | Meaning |
|---|---|---|
invalid_request | 400 | The request is malformed or out of bounds |
not_found | 404 | A referenced id is not in the store |
internal_error | 500 | Something failed that should not have |
A small, fixed code vocabulary with a descriptive message is a deliberate
choice. Clients branch on code; humans read message.
The messages carry the specifics, and they are unusually good at naming the fix:
unknown propagation_model `two-body` (expected `two_body`, `j2`, `sgp4`,
`numerical`, or `ephemeris`)
constellation `ph1` not found; generate it first
grid has 64800 points, exceeding the limit of 40000; use a coarser grid_deg
or a smaller regionEach names what was wrong and what to do instead.
Validation happens before the expensive work
Handlers check bounds, build and validate the grid, and only then touch the store and run the analysis. The comment in the coverage handler states the intent plainly:
// Build and validate the grid up front so an over-fine request is rejected
// before the (expensive) simulation runs.That ordering is why an over-large coverage request fails in milliseconds rather than after a minute of work.
The limits are the interesting part
Every endpoint is bounded. These are the constants, read from the source.
| Limit | Value | Bounds |
|---|---|---|
MAX_SATELLITES | 5000 | Constellation size |
MAX_DURATION_HOURS | 168 h | One week per request |
MIN_STEP_SECONDS | 1 s | Finest propagation interval |
MAX_COVERAGE_POINTS | 40000 | Grid points per coverage run |
MAX_GROUND_STATIONS | 50 | Stations per request |
MAX_MC_DRAWS | 20000 | Monte Carlo resilience draws |
MAX_TRADE_CANDIDATES | 48 | Trade-sweep candidates |
MAX_BER_POINTS | 25 | Es/N0 points per BER sweep |
MAX_BER_FRAMES | 2000 | Frames per BER point |
MAX_BER_TOTAL_FRAMES | 20000 | Points times frames |
MAX_THRESHOLD_FRAMES | 12 | Frames per MODCOD threshold |
These exist because the service is shared and a single request must not be able to consume it. That is the right instinct. But the specific values have a consequence worth stating directly.
Some measurements cannot be converged here
Compare the API’s ceilings against what BER sweeps and coding gain and measured MODCOD thresholds actually needed.
| Measurement | API default | API ceiling | What convergence needed |
|---|---|---|---|
| BER frames per point | 200 | 2000 | 1000000 |
| MODCOD frames per point | 6 | 12 | over 200, still rising |
The API caps BER at 2000 frames per point. Converging a single 1e-7 BER point took 1000000 frames, five hundred times the ceiling.
The MODCOD threshold endpoint caps frames at 12, which is below the CLI’s
default of 20, and the guide showed the threshold still climbing at 200. Those
are frame counts, not decibels.
The API says so itself. The rejection message is:
frames must be 1..=12 (CLI for higher confidence)That parenthetical is the architecture stating its own division of labor:
- The API is for exploration. Interactive, bounded, safe to expose, fast enough to sit behind a UI.
- The CLI is for results you intend to quote. Unbounded, local, as slow as the measurement requires.
A waveform number obtained from the API is a sketch. Note also that the API defaults are lower than the CLI defaults, not merely capped lower, so a call that sets no options is further from convergence over HTTP than it is on the command line.
Reproducing a published figure means using the CLI and recording its settings.
Route layout
46 endpoints, grouped by leading path segment. The largest groups:
| Prefix | Endpoints | Prefix | Endpoints |
|---|---|---|---|
/scenarios | 5 | /design | 2 |
/waveform | 4 | /import | 2 |
/analysis | 3 | /maneuver | 2 |
/fleet | 3 | /resilience | 2 |
/link | 3 | ||
/od | 3 |
The remainder are single endpoints: /simulate, /coverage, /eclipse,
/isl, /econ, /trade, /replan, /transfer, /stationkeep,
/conjunction, /platform, /stations, /orbits, /export, /ephemeris,
/fleets, /constellations, and /health.
/scenarios is the largest group because a stored scenario is exposed in four
representations, its CZML, OEM, ephemeris CSV, and contacts CSV, alongside the
scenario itself. It and /fleet/snapshots/{id} are the only routes with path
parameters; everything else is a POST with a JSON body.
Two prefixes cover fleets: /fleet/snapshots and
/fleets/{constellation_id}/reconcile, singular and plural.
They are the same domain concept reached under two different names. Worth knowing when you are searching the route table for something you cannot find.
Two spelling conventions
The API takes two_body; the CLI takes two-body.
Each is idiomatic for its interface, JSON snake case against POSIX kebab case, and neither accepts the other’s spelling. The error message names the valid values, so the failure is loud rather than silent.
Deploying it
Given the state model, the operational shape is fixed:
- One instance. Do not scale horizontally without an external store.
- Expect cold restarts to lose state. Anything durable belongs in files the client keeps.
- The limits are per request, not per client. Nothing here rate-limits a caller, so put that in front if the service is exposed.
- No authentication is built in. The only middleware in the router is request telemetry. There is no auth layer and no CORS layer, so the service assumes a trusted network or a gateway that supplies both.
Next steps
- Crate boundaries for what sits beneath the routes.
- API reference for the endpoints themselves.
- System overview for analysis flow.
main (pre-release)