Red Grid MGRS

I Built a GPS App That Makes Zero Network Calls

March 2026 · 8 min read

Red Grid MGRS is a military GPS navigator for iOS. It shows your position in MGRS coordinates, computes magnetic declination, runs dead reckoning and resection calculations, and generates tactical report templates. It does all of this without making a single HTTP request.

Not "minimal tracking." Not "privacy-focused." Zero. The app has no analytics SDK, no crash reporter, no telemetry endpoint, no health check ping, no update check. Put your phone in airplane mode and every feature works identically. There is no degraded state because there's nothing to degrade.

This wasn't the path of least resistance. Here's how the architecture works and what I had to solve to make it viable.

Why Zero Network?

The app is built for military users running land navigation. These are people who operate in environments where network connectivity is either unavailable or actively dangerous. A GPS app that phones home is a liability.

But beyond the tactical use case, I think most mobile apps have no business making the network calls they make. Your calculator app doesn't need analytics. Your flashlight doesn't need a crash reporter. And a GPS app that streams your coordinates to a telemetry server is doing something fundamentally hostile to the user, even if it's "anonymized."

So the rule was simple: the app makes zero outbound connections during normal operation. The only exception is the App Store IAP flow, which is initiated by the user tapping a purchase button and handled entirely by Apple's StoreKit framework.

The Architecture

The app is React Native (Expo SDK 52), pure JavaScript, hooks-based. About 3,500 lines of application code across ~25 files. Here's the structure that matters:

src/
  hooks/
    useLocation.js      ← GPS (ephemeral, in-memory only)
    useSettings.js      ← loads from AsyncStorage
    useGridCrossing.js  ← derived from useLocation
    useShakeToSpeak.js  ← accelerometer listener
  utils/
    mgrs.js             ← DMA TM 8358.1 conversion (pure math)
    tactical.js         ← geodetic calculations (pure math)
    storage.js          ← AsyncStorage wrapper (never stores location)

The key insight: location data and persisted data live in completely separate worlds.

GPS data is ephemeral

Your coordinates exist in React state and nowhere else. When the component unmounts, the data is gone. There's no location history, no breadcrumb trail, no last-known-position cache.

// useLocation.js — data flow
//
// GPS satellite → phone receiver → expo-location → React state → render
//                                                              → gone
//
// That's it. No fork in the pipeline. No "also send to..."

The useLocation hook watches the device GPS at 1-second intervals with a 1-meter distance threshold. It updates React state. The state drives the UI. When the app closes, the state is garbage collected. At no point does a coordinate touch AsyncStorage, a file, or a network socket.

There's a subtlety here that matters: I suppress redundant re-renders by comparing incoming coordinates against the previous position with a threshold of ~0.1 meters. If you're standing still, the GPS jitter doesn't cause a render cascade. This is a performance optimization, but it also means the app isn't even processing your position data more than necessary.

const COORD_THRESHOLD = 0.000001; // ~0.1m at equator

const updateLocationIfChanged = useCallback((newLoc) => {
  const prev = prevCoords.current;
  if (
    prev &&
    Math.abs(newLoc.lat - prev.lat) < COORD_THRESHOLD &&
    Math.abs(newLoc.lon - prev.lon) < COORD_THRESHOLD
  ) {
    return; // Position unchanged, skip re-render
  }
  prevCoords.current = { lat: newLoc.lat, lon: newLoc.lon };
  setLocation(newLoc);
}, []);

Persisted data is never location

The app does persist some data using AsyncStorage. Here's the complete list:

That's it. Seven keys. No location history. No session data. No device fingerprint. No user ID.

Waypoints are the one case where a coordinate hits disk — but only when the user explicitly taps "Save Waypoint" and names it. It's user-initiated, user-labeled, and user-deletable. It's their data, stored on their device, for their use.

All math is local

The MGRS conversion is implemented from the DMA (Defense Mapping Agency) Technical Manual 8358.1. It's about 300 lines of pure math — WGS84 ellipsoid constants, UTM projection, zone letter assignment, 100km square identification. Zero dependencies. No geocoding API. No coordinate lookup service.

// The entire MGRS pipeline is deterministic pure functions:
//   latitude, longitude → UTM easting/northing → zone + square + digits
//
// Input: two floats
// Output: a string like "18S UJ 23456 78901"
// Side effects: none

Same story for magnetic declination (WMM 2025 coefficients baked in), dead reckoning (Vincenty formula on WGS84), resection (two-bearing intersection), and solar calculations. Every computation runs locally, deterministically, with no external data.

What You Give Up

This architecture has real costs. I want to be honest about them.

No crash reporting. When the app crashes, I don't know about it unless a user emails me. I can't see stack traces, device models, or OS versions. I compensate with aggressive defensive coding — every async call is wrapped in try/catch with timeouts, every external module is lazy-loaded with fallbacks, every JSON parse has corruption protection. The app has never crashed in production (that I know of), but I also can't prove that.

No usage analytics. I don't know how many people use the app daily. I don't know which features are popular. I don't know if anyone uses the resection tool or if the pace counter is just noise. I make product decisions based on what I think is useful, not data. App Store Connect gives me download counts and that's about it.

No remote configuration. If I bake in a wrong constant — say, a WMM coefficient — I can't hot-fix it. I have to push an App Store update and wait for review. This means I test math functions obsessively. The test suite has 142 tests, and the MGRS converter alone has edge cases for every UTM zone boundary, the Norway/Svalbard special zones, and both poles.

No A/B testing. I can't test two different UIs against each other. The version I ship is the version everyone gets. This is honestly fine for a utility app, but it means design decisions are based on gut feel.

What You Get Back

The benefits are less obvious but, I think, more important.

Absolute trust. The app is open source. Anyone can verify the zero-network claim by reading the code. There's no "trust us, we anonymize it." There's nothing to anonymize because there's nothing to collect. For military users operating in sensitive environments, this matters.

Works everywhere. Underground parking garage. Middle of the ocean. Remote mountainside with no cell coverage. Airplane mode. The app doesn't care. There's no loading spinner waiting for a server. There's no "requires internet connection" disclaimer in the App Store listing.

No ongoing cost. The app has no backend. No server to maintain. No database to scale. No API rate limits to worry about. My infrastructure cost is $0/month. The only recurring cost is the Apple Developer Program at $99/year. This means the app can exist forever without generating revenue, which is a luxury that changes how you make product decisions.

Faster than you'd expect. No network waterfall means the app is ready the instant it launches. GPS lock depends on satellite geometry, but the UI is interactive immediately. There's no "connecting to server" state. The coordinate display shows a placeholder until the first GPS fix, then it's real-time at 1Hz.

The Hard Part: Hardening Without Telemetry

When you can't see production errors, you have to prevent them. Every I/O boundary in the app is wrapped defensively:

// Every AsyncStorage call: existence check → try/catch → timeout → default
export async function loadSettings() {
  try {
    if (!AsyncStorage || !AsyncStorage.multiGet) {
      return DEFAULTS;  // Module missing? Return defaults.
    }

    const items = await Promise.race([
      AsyncStorage.multiGet([...keys]),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Storage timeout')), 5000)
      )
    ]);

    if (!items || !Array.isArray(items)) return DEFAULTS;

    // Parse each value individually — one corrupted key
    // doesn't take down the others
    // ...
  } catch (err) {
    return DEFAULTS;  // Total failure? Still works.
  }
}

The pattern is the same everywhere: check if the module exists, wrap the call, race against a timeout, validate the response shape, fall back to a sensible default. The app should never show the user an error screen. If storage is corrupted, you get default settings. If GPS fails, you get a clear message and a retry button. If the magnetometer is unavailable, the compass arrow degrades to absolute bearing.

This is more work than dropping in Sentry and calling it a day. But it produces a fundamentally more resilient app.

Should You Do This?

For most apps, probably not. If you're running a SaaS product with user accounts, you need a backend. If you're a team of 50, you need crash reporting to stay sane. If your business model depends on understanding user behavior, you need some form of analytics.

But if you're building a utility — a calculator, a converter, a reference tool, a sensor reader — ask yourself: does this actually need a network connection? If the answer is "only for analytics," maybe the answer is that it doesn't need one at all.

The bar should be higher than "well, everyone adds analytics." Your user's data is theirs. If you don't need it, don't take it.

Red Grid MGRS Free on the App Store. Source code on GitHub.