There's an uncomfortable truth in any app that records where you drove: a route almost always starts and ends somewhere you'd rather not broadcast. Your house. Your work. Your kid's school. Share that track publicly and you've handed a stranger a map straight to your front door.
Tourismo is built around sharing drives, so I had to solve this before I let anyone share anything. The feature is called Privacy Zones, and the core principle is simple: the sensitive part of the track is removed in the browser, before it's ever uploaded. The server never sees it. It can't leak what it never received.
Tourismo is a PWA, not a native app. That's a deliberate choice — one codebase, instant updates, no app-store gatekeeping — but it means I'm living entirely inside the browser sandbox. No native location framework, no background daemon, no on-device geofencing API. The GPS comes from navigator.geolocation, the track lives in JavaScript memory, and any "privacy processing" has to be plain code running on the main thread before a fetch.
That constraint turned out to be a feature. Because everything already happens client-side, doing the trimming client-side wasn't extra work — it was the natural place for it.
A privacy zone is just a centre point and a radius. The user drops one on their home, one on work. When a drive is saved, the raw points run through trimWithPrivacyZones before anything is uploaded:
export function trimWithPrivacyZones<P extends { lat: number; lng: number }>(
points: P[],
zones: PrivacyZone[],
): TrimResult<P> {
if (zones.length === 0 || points.length === 0) {
return { trimmed: points, zonesApplied: false };
}
const inZone = (p: P, buffer = 0): boolean =>
zones.some(z => haversineDistance(p, z.center) <= z.radiusMeters + buffer);
let start = 0;
while (start < points.length && inZone(points[start]!, ZONE_APPROACH_BUFFER_M)) start++;
let end = points.length - 1;
while (end >= start && inZone(points[end]!, ZONE_APPROACH_BUFFER_M)) end--;
if (start > end) return { trimmed: [], zonesApplied: true };
// ...also drop any mid-track points that pass back through a zone
}
It walks in from both ends, dropping points until the track leaves the zone, then sweeps the middle for any point that passes back through one. Two details matter. First, there's a 200-metre approach buffer beyond the stated radius:
export const ZONE_APPROACH_BUFFER_M = 200;
A pure radius cut isn't enough. The approach and departure roads to your house are nearly as identifying as the house itself — anyone can see the track stops dead at the end of one specific lane. Trimming an extra 200m past the radius hides the approach line, not just the address.
Second — and this is the bit I'm quietly proud of — the exact same function runs on the server's retroactive redact endpoint. If you add a privacy zone after you've already shared drives, the server re-trims the stored tracks with the identical code path. Client trim on upload, server trim on demand, one implementation. They can never drift apart and accidentally redact differently.
The trimmed points are what get compressed and uploaded; the raw ones never leave memory. But the bigger decision wasn't technical. Every new drive defaults to private visibility, not public:
const [visibility, setVisibility] = useState<Visibility>('private');
You have to actively choose to share. The save flow also records a privacyZonesApplied flag so the UI can honestly tell you trimming happened. Privacy that you have to find and switch on isn't privacy — it's a setting most people never reach. Defaulting to private and trimming automatically means the safe path is the lazy path, and the lazy path is the one everyone actually takes.
Do the redaction as close to the source as you possibly can. Every layer the raw data passes through — your API, your logs, your database, your backups — is another place it can leak or be subpoenaed. By trimming in the browser, the most sensitive coordinates a user owns simply never enter my system. That's not just good engineering; it's the only privacy promise I can actually keep, because I can't lose data I never held.