Privacy-Preserving Proximity Discovery with MongoDB Geospatial Queries I’m building a local meetup app — the kind where you discover people nearby who share your interests, then meet up in person. One of the core features is proximity discovery: showing users who’s nearby. It sounds simple until you actually think about what “showing who’s nearby” means from a privacy and safety standpoint.
Here’s the problem I ran into, and how I solved it with MongoDB’s geospatial features.
The problem: proximity without exposing exact coordinates
Proximity discovery fundamentally requires distance calculations — the app needs to know how far apart two users are to answer “who’s near me.” The naive approach is to just return each nearby user’s raw latitude and longitude to the client and calculate distance in the UI.
That’s a serious mistake. Returning raw coordinates to other users creates real physical safety and stalking risk — anyone could pinpoint exactly where another user lives, works, or is standing right now. For an app built specifically to be safety-conscious, that’s the opposite of acceptable.
So the actual requirement wasn’t just “show distance” — it was “show distance without ever exposing the underlying location.”
The approach: index precisely, expose vaguely
The solution splits into two halves that need to stay separate: what gets stored and queried versus what gets returned to the client.
Storage and querying still needs full precision — you can’t do accurate distance calculations on fuzzy data. Locations are stored as GeoJSON Point objects and indexed with MongoDB’s 2dsphere index, which enables efficient geospatial queries like $geoNear (find documents near a point, sorted by distance) and $geoWithin (find documents inside a boundary).
js
// Location schema — GeoJSON Point with a 2dsphere index
const locationSchema = new Schema({
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
location: {
type: { type: String, enum: ['Point'], default: 'Point' },
coordinates: { type: [Number], required: true }, // [longitude, latitude]
},
});
locationSchema.index({ location: '2dsphere' });
A typical discovery query looks something like this — find users within a radius, sorted by distance, excluding anyone already filtered out (blocked users, the requester themself, etc.):
js
const pipeline = [
{
$geoNear: {
near: { type: 'Point', coordinates: myCoords },
distanceField: 'distanceMeters',
maxDistance: maxKm * 1000,
spherical: true,
query: {
userId: { $nin: excludedIds },
},
},
},
];
const nearbyUsers = await Location.aggregate(pipeline);
That query is accurate and fast (with the 2dsphere index, it’s a proper indexed geospatial lookup, not a brute-force scan). But its raw output — exact coordinates and precise distance — never goes straight to the client.
The privacy layer: a sanitizing DTO
Between the raw query result and the API response, there’s a transformation step — what I ended up calling a privacy DTO sanitizer. Its only job is to take the precise result and strip it down to the minimum a client actually needs.
js
const COORD_REDACTED = 'REDACTED';
function toDiscoveryDTO(rawResult, user) {
const distanceKm = Math.round((rawResult.distanceMeters / 1000) * 10) / 10;
return {
user: {
id: user._id,
name: user.name,
username: user.username,
bio: user.bio,
city: user.city,
occupation: user.occupation,
interests: user.interests,
avatarHue: user.avatarHue,
isVerified: user.isVerified,
},
distanceKm,
distanceLabel: formatDistance(rawResult.distanceMeters), // "1.2 km away"
coordinatesRedacted: COORD_REDACTED,
// note: no `coordinates` field at all — it simply never leaves the server
};
}
function formatDistance(meters) {
const km = meters / 1000;
if (km < 1) return `${Math.round(meters)} m away`;
return `${km.toFixed(1)} km away`;
}
The key design decision here isn’t really the rounding logic — it’s that exact coordinates are structurally excluded from the response shape, not just rounded or fuzzed. A rounded coordinate can still be reverse-engineered with enough samples over time; a response that never contains a coordinate field at all can’t leak one by accident, even if someone forgets to sanitize a new field later.
(If you’re newer to the idea of designing response shapes around what should never be exposed, [link: your schema-design post] covers the same principle in more depth.)
Bug: inconsistent coordinate ordering crashing queries
One recurring issue while building this: GeoJSON expects coordinates as [longitude, latitude], but it’s extremely easy — as a developer, or worse, from client input — to accidentally pass [latitude, longitude] instead. Most people naturally think “lat, lng” since that’s the order most map UIs and casual conversation use, which is the exact opposite of GeoJSON’s actual order.
That mismatch doesn’t fail loudly. Depending on the values, it can silently produce a valid-but-wrong point (nowhere near where it should be), or, if the value is out of range for a valid coordinate, throw an unhelpful database-level error deep inside a $geoNear aggregation.
The fix was validating and normalizing coordinates at the boundary, before they ever reach a query:
js
const coordinateSchema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
});
function toGeoJsonPoint(input) {
const { latitude, longitude } = coordinateSchema.parse(input);
return {
type: 'Point',
coordinates: [longitude, latitude], // explicit: GeoJSON order, not lat/lng order
};
}
Two things do the real work here: strict range validation (catching obviously invalid values immediately, with a clear error, instead of letting them hit MongoDB), and a single conversion function that’s explicit about the reordering — so “which order does GeoJSON want” only has to be gotten right in one place in the whole codebase.
What I’d tell someone building location features
- Precision and exposure are two different decisions. Store and query at full precision; decide what to expose to the client as a completely separate step, not as an afterthought on the same object.
- Exclude sensitive fields structurally, not just by rounding them. A response shape that has no coordinate field can’t leak one. A response that has a “fuzzed” coordinate field is one refactor away from someone accidentally returning the precise version.
- Validate coordinate order explicitly.
[lat, lng]vs[lng, lat]is a classic, silent bug — don’t rely on remembering it, encode it in one conversion function everyone uses. - Use 2dsphere and
$geoNearinstead of hand-rolled distance math. MongoDB’s geospatial indexing is both faster and more correct (it accounts for the Earth’s curvature) than computing straight-line distance in application code.
FAQ
How do you hide exact GPS coordinates in a proximity API?
Return a derived value — like a distance label or a coarse area name — instead of raw coordinates. The safest approach excludes the coordinate field from the response shape entirely, rather than rounding or fuzzing it.
What’s the difference between rounding coordinates and excluding them?
Rounded coordinates are still coordinates — with enough repeated samples over time, they can be triangulated back toward a precise location. A response with no coordinate field at all has nothing to reverse-engineer.
Why does MongoDB want [longitude, latitude] instead of [latitude, longitude]?
It follows the GeoJSON spec, which orders coordinates as x, y — longitude first. It’s counterintuitive because most people think and speak in “lat, long” order, which is why this is such a common source of silent bugs.
This privacy-first approach to location data follows the same philosophy as the app’s other safety features — build the safeguard into the data model and response shape itself, not as a rule developers have to remember to follow later. Structural constraints don’t get forgotten in code review the way conventions do.
Have you built location-based features with different privacy trade-offs? I’d be curious to hear how you approached it — reach out via the [Contact page].
Related: Building an SOS Emergency Pipeline: From One Tap to Dispatch · knowabteverything.com
2 thoughts on “Privacy-Preserving Proximity Discovery with MongoDB Geospatial Queries”