iOS app that runs a local HTTP server on 127.0.0.1:17580 and acts as a Nightscout-compatible bridge between a CGM uploader and a Nightscout reader (e.g. iAPS) running on the same device.
- Accepts incoming HTTP traffic on
http://127.0.0.1:17580(loopback only). - Forwards everything to a configured Nightscout server (
SettingsManager.nightscoutURL) and returns the upstream response to the client. - Intercepts a few endpoints and serves them locally so an uploader can authenticate and read its own data even when upstream is unreachable or has a mismatched API secret:
GET /api/v1/verifyauth— always returns authorized.GET /api/v1/statusand/api/v1/status.json— returns a synthetic fully-authorized status response.GET /api/v1/entriesand/api/v1/entries.json— served from the local store, withfind[dateString][$gte|$lte]andcountquery parameters honored.
- Persists
POST /api/v1/entriespayloads to local storage (UserDefaults) so the GET endpoint above can replay them. Each entry is keyed by_id; if missing, a stable id is synthesized fromtype + date + device. - Queues outbound forwards for retry while offline (
NWPathMonitor-driven) and replays them when the network returns. 4xx upstream responses are treated as terminal and dropped from the queue; 5xx and transport errors stay queued.
Local CGM entries are kept for 1 hour (sliding window). See EntryStore.retentionWindow in NSBridge/EntryStore.swift.
The retry queue keeps pending requests for 7 days — see RequestQueue.maxQueueAge.
To keep the HTTP listener responsive while the app is backgrounded, NS Bridge:
- Plays a silent audio loop (
AVAudioSession.playback+.mixWithOthers) — see NSBridge/BackgroundAudioPlayer.swift. This requiresUIBackgroundModes = ["audio", ...]in the Info.plist. - Requests
BGTaskSchedulerapp refresh time as a fallback (com.nsbridge.app.refresh).
The audio is muted (mainMixerNode.outputVolume = 0) and configured with .mixWithOthers, so it does not interrupt other audio.
⚠️ The silent-audio keepalive is sufficient for personal/sideload use. App Store review may flag it for distribution.
Tap the gear icon to set the upstream Nightscout URL (default: https://your.nightscout.cloud). Changing it restarts the listener.
NS Bridge does not store a Nightscout API secret of its own. The token is supplied by the client (e.g. the CGM uploader) on each request and is forwarded as-is to the upstream Nightscout host.
- The client sends its API secret in the standard Nightscout
api-secretheader (the SHA-1 hex of the plaintext secret) — or as a?token=…query parameter for token-based auth. - For locally-served endpoints (
/api/v1/verifyauth,/api/v1/status[.json],GET /api/v1/entries[.json]), NS Bridge always responds as authorized regardless of the header value, so the uploader can keep working even if its secret does not match upstream. - For everything else (including
POST /api/v1/entries), the request is forwarded toSettingsManager.nightscoutURLwith the client'sapi-secret/tokenpreserved. OnlyHost,Connection, andContent-Lengthare stripped — seeforwardRequestin NSBridge/WebServer.swift. - This means the client must be configured with the secret/token that the upstream host accepts. NS Bridge is a transparent pass-through for auth.
Example — point an uploader at http://127.0.0.1:17580 and configure its API secret as you would for the real Nightscout. The bridge will hand the same header to upstream:
curl -X POST http://127.0.0.1:17580/api/v1/entries \
-H "Content-Type: application/json" \
-H "api-secret: $(echo -n 'your-plaintext-secret' | shasum | awk '{print $1}')" \
-d '[{"device":"test","sgv":110,"date":'"$(($(date +%s)*1000))"',"dateString":"'"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"'","direction":"Flat","type":"sgv"}]'| CGM | Status | Notes |
|---|---|---|
| Instara | ✅ Works | Uploads directly via the Nightscout API, so the bridge can forward and cache transparently. |
| Syai | ❌ Unsupported | Data is not transmitted directly to Nightscout — it goes to Syai Cloud first, which then pushes to the Nightscout host out-of-band. The bridge never sees the traffic and cannot intercept or cache it. |
NSAppTransportSecurity allows arbitrary loads so the proxy can forward to plain http:// Nightscout servers. See NSBridge/Info.plist.
The listener is bound to 127.0.0.1 via NWParameters.requiredLocalEndpoint, so it is not reachable from other devices on the same Wi-Fi.
| Method | Path | Behavior |
|---|---|---|
| GET | /api/v1/verifyauth |
Local — always authorized |
| GET | /api/v1/status / status.json |
Local — synthetic authorized status |
| GET | /api/v1/entries[.json] |
Local — served from device storage |
| POST | /api/v1/entries[.json] |
Stored locally and forwarded to upstream |
| any | anything else | Forwarded to upstream Nightscout server |
Forwarded requests strip Host, Connection, and Content-Length headers (URLSession sets these correctly itself).
- iOS 17+
- Xcode 15+
- Open
NSBridge.xcodeproj. - Select a target (simulator or device).
- Build & Run.
# Health-style probe (intercepted)
curl http://127.0.0.1:17580/api/v1/status.json
# Submit a CGM reading
curl -X POST http://127.0.0.1:17580/api/v1/entries \
-H "Content-Type: application/json" \
-H "api-secret: $(echo -n 'mysecret' | shasum | awk '{print $1}')" \
-d '[{"device":"test","sgv":110,"date":'"$(($(date +%s)*1000))"',"dateString":"'"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"'","direction":"Flat","type":"sgv"}]'
# Read it back (within retention window)
curl 'http://127.0.0.1:17580/api/v1/entries?count=10'