DEV Community

Cover image for Where was this taken, and which way was I facing? Geolocation & sensors in a PWA (FieldKit #4)
Oleksandr Trukhnii
Oleksandr Trukhnii

Posted on

Where was this taken, and which way was I facing? Geolocation & sensors in a PWA (FieldKit #4)

This is part 4 of FieldKit, a series where I build one real Progressive Web App and use it to dig into what modern PWAs can actually do. FieldKit is a field-notes app — open source (on GitHub). It already works offline, installs, and captures photos and audio. Now we answer two questions every field note should carry: where was it taken, and which way was I facing?

Location turns a note into a place

A photo of a trail junction is useful. A photo of a trail junction pinned to 46.812, 8.224 and tagged "facing NE" is a record. For a field tool, location is the metadata that makes everything else searchable later.

The web gives you two relevant sensors here:

  1. Geolocation — latitude/longitude (and accuracy), via the Geolocation API.
  2. Orientation — a compass heading, derived from the device's magnetometer via device-orientation events.

They look similar from the outside ("ask the device where/how it is") but they behave very differently in practice. Let's take them one at a time.

Geolocation: one call, three things to respect

Getting a position is a single call. Doing it well means respecting three things: accuracy, timeouts, and permission.

export function getPosition(
  options = { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
) {
  if (!("geolocation" in navigator)) {
    return Promise.reject(new Error("Geolocation isn't available here."));
  }
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (pos) =>
        resolve({
          lat: pos.coords.latitude,
          lng: pos.coords.longitude,
          accuracy: pos.coords.accuracy, // metres
        }),
      (err) => reject(new Error(describeGeoError(err))),
      options
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

Those options matter more than people assume:

  • enableHighAccuracy: true asks for GPS-grade precision instead of a coarse Wi-Fi/IP estimate. It costs battery and time — use it for a field app, skip it for "which city is this user in."
  • timeout caps how long you'll wait. Without it, a device that can't get a fix leaves your promise hanging forever.
  • maximumAge: 0 forbids a cached position — we want now, not where the phone was ten minutes ago.

And accuracy isn't decoration: it's the radius in metres the browser is confident about. A reading with accuracy: 2000 is a Wi-Fi guess, not a GPS lock — worth surfacing to the user rather than pretending 5 decimal places is truth.

Handle every failure mode

Geolocation fails in distinct ways, and users deserve a specific message for each:

function describeGeoError(err) {
  switch (err.code) {
    case err.PERMISSION_DENIED:    return "Location permission denied.";
    case err.POSITION_UNAVAILABLE: return "Location unavailable right now.";
    case err.TIMEOUT:              return "Timed out getting your location.";
    default:                       return "Couldn't get your location.";
  }
}
Enter fullscreen mode Exit fullscreen mode

The compass: welcome to sensor fragmentation

Heading is where the web gets genuinely messy, and pretending otherwise is how you ship a broken compass. There's no clean "give me north" API. You listen to device-orientation events and derive a heading — and every platform does it slightly differently.

Two big forks:

1. iOS gates it behind a permission call that needs a user gesture. Since iOS 13, you must call DeviceOrientationEvent.requestPermission() from inside a click/tap handler, or you get nothing:

if (
  typeof DeviceOrientationEvent !== "undefined" &&
  typeof DeviceOrientationEvent.requestPermission === "function"
) {
  const state = await DeviceOrientationEvent.requestPermission(); // iOS gate
  if (state !== "granted") throw new Error("Compass permission denied.");
}
Enter fullscreen mode Exit fullscreen mode

2. The heading value lives in different places. iOS hands you a ready-made webkitCompassHeading (degrees clockwise from north). Everyone else fires an absolute orientation event where you derive heading from alpha — and some browsers name that event deviceorientationabsolute rather than deviceorientation. So you listen for both and read whichever field exists:

const onOrient = (e) => {
  let heading;
  if (typeof e.webkitCompassHeading === "number") {
    heading = e.webkitCompassHeading;   // iOS
  } else if (e.absolute && typeof e.alpha === "number") {
    heading = (360 - e.alpha) % 360;    // convert absolute alpha to heading
  } else {
    return; // no usable heading in this event — wait for the next one
  }
  finish(heading);
};

window.addEventListener("deviceorientationabsolute", onOrient);
window.addEventListener("deviceorientation", onOrient);
Enter fullscreen mode Exit fullscreen mode

And because plenty of devices have no magnetometer at all (most laptops), you need a timeout that gives up gracefully instead of waiting forever:

const timer = setTimeout(() => {
  cleanup();
  if (!settled) reject(new Error("No compass data on this device."));
}, timeout);
Enter fullscreen mode Exit fullscreen mode

In FieldKit the heading is strictly a bonus: when the user attaches a location, I try to read a heading too, but a failure never blocks saving the note.

const pos = await getPosition();
let heading = null;
try {
  heading = await getHeading();
} catch {
  /* no magnetometer / denied — carry on without a heading */
}
pendingLocation = { lat: pos.lat, lng: pos.lng, heading };
Enter fullscreen mode Exit fullscreen mode

That "best-effort sensor, required core data" split is the pattern I reach for with any flaky capability: never let the nice-to-have take down the must-have.

The big limitation: no background geolocation

Here's the thing to internalise, because it shapes what you can even build: the web cannot track location in the background. There is no web equivalent of a native app's "always allow" background location. getCurrentPosition and even watchPosition only run while your page is open and focused. Close the tab or lock the phone and location updates stop.

For FieldKit that's fine — we capture a position at the moment you save a note. But if you were dreaming of a web-based run tracker that records your route with the screen off, that's simply not a thing the web platform allows, by design, for privacy reasons. Know this before you promise it to anyone.

Honest support picture

  • Geolocation API: universal, including Safari and Firefox — but secure-context only (HTTPS or localhost) and always permission-gated. watchPosition exists for live updates while the page is open.
  • Device orientation / compass: broadly available on devices that have a magnetometer (i.e. phones), but the API surface is fragmented — webkitCompassHeading (iOS) vs absolute alpha (others), plus the iOS requestPermission() gate and the HTTPS requirement. Treat a heading as best-effort, never guaranteed.
  • No background geolocation anywhere on the web. Full stop.
  • The newer Generic Sensor API (AbsoluteOrientationSensor, Magnetometer) is a cleaner path on Chromium but isn't in Safari, so device-orientation events remain the portable choice today.

As always, verify on caniuse: Geolocation and DeviceOrientation before you depend on specifics.

How this compares to Electron

  • Electron runs on desktops, which usually have no GPS and no magnetometer — so geolocation falls back to a coarse Wi-Fi/IP lookup and a compass heading basically isn't a thing. Where Electron wins is the background story: with Node and OS access it can keep working with location-ish data while minimised, run as a background process, and integrate with native location services the web can't touch.
  • The PWA is the opposite trade-off, and for a field app it's the right one: the user is holding a phone that has real GPS and a real compass, and the same code that would give Electron a fuzzy desktop guess gives the PWA a precise, heading-aware fix. The cost is the platform's privacy guardrails — permission prompts, secure context, and no background tracking.

Put bluntly: for anything mobile and location-driven, the PWA has better hardware under it than a desktop Electron app, and pays for it only with the web's (reasonable) privacy constraints.

Try it

Serve FieldKit over localhost (or HTTPS on a phone), tap 📍, and grant location.

A note with geolocation

On a phone with a magnetometer you'll also get a heading like "🧭 NE 45°" on the note. On a laptop you'll get the position and a graceful "no compass" — exactly as designed.

git clone https://github.com/JohnJunior/FieldKit.git
cd FieldKit
npx serve .
Enter fullscreen mode Exit fullscreen mode

Next up (FieldKit #5): getting data in and out — reading and writing real files with the File System Access API, sharing entries via the Web Share API, and the honest fallbacks for the browsers that support neither.

Top comments (0)