Back to writing
Backend infrastructure / provider normalization / SDKs / 2026
One mountain backend, fourteen data providers
PeakHut is the backend behind my outdoor apps. It connects to Météo-France, SLF, IGN, Copernicus, GLIMS, and other sources, then returns one consistent response through Swift and TypeScript SDKs.
The hard part is getting a dozen mountain data sources to agree. Their field names, units, languages, update schedules, and failure modes are all different. The app still needs one dependable answer.
Mountain apps are unforgiving about it. Someone taps a point on the map and wants to know: what am I standing in, what's the latest avalanche bulletin, how steep is this, are there alerts, where's the nearest hut, what could go wrong here. Those answers live in completely different places: national weather agencies, avalanche services, terrain datasets, public route databases, glacier catalogs, local condition reports, plus a few hazard layers I generate myself.
PeakHut handles that work in a Bun backend with provider adapters, normalized models, Postgres, background refresh jobs, protected routes, raw passthrough, Slab-compatible migration endpoints, and first-party Swift and TypeScript SDKs. Traverse iOS and Traverse Web use it today. It is single-tenant and only serves my own apps.
I built PeakHut to learn what it really takes to make an API-dependent product hold up: how integrations behave when a provider is slow, unavailable, or changes shape; where caching helps; and how much work sits between an upstream response and a feature I can rely on. It's still a small, single-tenant system, and that constraint is part of the exercise.
The provider problem
Météo-France uses French fields and a 1-to-5 avalanche scale. SLF covers Switzerland. IGN provides metre-scale French elevation, while Copernicus trades detail for global coverage. Some sources return clean JSON, some need careful feed parsing, and some are tiles I generate and publish to object storage. Each source has its own refresh schedule and failure mode.
I put those rules in one capability registry. It records which provider covers each dataset, where it applies, how fresh it needs to be, and whether the raw payload is available. The current registry covers fourteen providers in roughly eighteen entries.
A resolver chooses the provider by geography. Points in France use IGN elevation, with Copernicus as the fallback outside its coverage. Avalanche bulletins switch between Météo-France and SLF at the border. The rule lives in one backend file. Here is a trimmed version:
This was new territory for me: defining product contracts I could extend as the apps changed, knowing some would eventually need a full migration, while building the infrastructure underneath them. As a designer, I was probably going too deep into software architecture. That was also where the fun was.
The normalized contract
For a map-tap sheet, the app calls /v1/locations/summary once. The response includes the point, elevation, surrounding areas, latest bulletin, nearest observation, alerts, and map-ready GeoJSON overlays.
Route analysis starts with a GPX file and returns conditions plus overlays. Narrow endpoints are still available when a screen only needs one layer.
Every record includes the provider, fetch time, normalization time, and resolver rationale. When an app shows something odd, that provenance usually tells me whether the source data is wrong or my mapping is wrong.
The hard parts
Cache policy affects safety. Avalanche bulletins refresh every three hours and expire after six. Elevation is cached for a year. Less reliable hazard feeds get a one-hour cache; if a source is down, PeakHut serves the last good copy with its fetch time so the app can show its age. All of this data is planning context, never a go/no-go call.
Normalization is most of the work. French and Swiss bulletins use different fields and languages. Avalanche risk combines a 1-to-5 number with labels. Elevation arrives in UTM, RGF93, or WGS84. PeakHut renames fields, reprojects coordinates, and converts units before an app sees the response.
GLIMS, Georisques, Camptocamp, and the condition feeds have parser fixtures because their upstream shape can change without warning. The resolver has coverage tests too: a wrong bounding box can return the wrong country's avalanche bulletin.
Security and operational boundaries
Provider keys, tile secrets, cron secrets, and long-lived platform keys stay on the backend.
There are three zones. /health is open. The real endpoints (/openapi.json, /ops, /v1/*, /v2/*) need an x-api-key with a scope like platform.read or platform.route. The internal refresh jobs sit behind a separate x-cron-secret. Rate limiting runs per key and per IP, with the heavier tile traffic on its own budget.
I can change the provider, cache, or artifact behind a feature while the app keeps calling the same method.
SDKs as product infrastructure
The apps use a Swift package and a TypeScript client. Both stay thin: they mirror the response shape, expose named flows, and leave provider logic on the server.
The Swift SDK has async methods like locationSummaryWithOverlays(at:), searchMountainHuts, crevassePoint, parseGPX, routeConditions, and snowCoverTileJSON. The TypeScript SDK mirrors the same flows for web, React Native, SSR, and Node, with an injectable fetch for server environments and tests.
The SDKs make common calls one line long and keep the server contract visible. When that contract changes, the backend, OpenAPI, Swift models, and TypeScript models change together.
What it's actually running
I built PeakHut after copying the same provider clients between several outdoor apps. The shared plumbing now lives in one backend and each app keeps its own interface.
- Traverse iOS uses PeakHut for map overlays, route planning, avalanche and hazard layers, snow cover, LiDAR terrain, glacier data, and huts. A map tap is one call:
locationSummaryWithOverlays(at:).
- Traverse Web uses its own backend proxy so provider keys never reach the browser. It requests viewport-scoped glacier outlines, LiDAR catalogs, snow cover, and map layers.
- Slab migration uses compatibility routes for French bulletins, global avalanche zones, terrain overlays, weather profiles, and alerts. Existing consumers can move over gradually.
- Outpost uses the shared mountain-hut records, search, route context, and R2-backed photos through PeakHut.
- Anything I build next (iOS, web, React Native, server-side) gets the same contract on day one, with rendering and caching left to the app.
What's still rough
The adapters, normalized contracts, protected routes, background jobs, raw diagnostics, migration endpoints, and SDKs are working. The next job is completing the OpenAPI definition so it can generate the Swift and TypeScript models. Those models are still mirrored by hand and can drift.
Long refresh jobs also need a supervised queue, provider health needs real scoring, and rate limiting needs to move out of process memory. PeakHut is single-region and single-tenant today, and the OpenAPI coverage is partial.
I haven't tested any of this under massive traffic. Across my apps and TestFlight builds, roughly 300 to 400 people use the products, which isn't enough load to properly stress the infrastructure or show me where it breaks. The next lesson may just be Railway, the hosting provider, sending me a bill twenty times larger.