Discovery — Domain Spec¶
Discovery is the read-and-aggregate domain: it answers "what's near me, who's near me, and who is this person" by querying the write domains, never by owning their state. This page is the rules for what's discoverable, at what zoom, under what visibility, and how the search and profile surfaces are shaped.
Mental model¶
Discovery is a projection layer with one small piece of its own state.
- It reads, it doesn't own. Events, locations, moments, friendships, presence, and user rows all live in their own domains. Discovery queries them (directly against the DB and Redis) and composes response shapes. It writes nothing back except the saved pin.
- The saved pin is the only owned state. A per-user "area
selector" pill (
GET/PUT /discovery/pin) remembers where the Find tab is centred between sessions. - Four read shapes. The map (viewport + zoom), the radar (radius), search (natural-language + kind-scoped), and the profile card (one person, aggregated).
Surface area¶
All routes are under /discovery. Everything except the profile card
requires auth; the profile card takes optional auth.
| Route | Returns |
|---|---|
GET /discovery/map |
Viewport markers for a bounding box + zoom |
GET /discovery/radar |
Friends + strangers within a radius, sorted by distance |
GET /discovery/locations/:id |
Location detail card (highlights, hours, upcoming events) |
GET /discovery/search |
Unified natural-language intent, fanned to events + locations |
GET /discovery/search/events |
Event search near the pin |
GET /discovery/search/locations |
Place search near the pin |
GET /discovery/search/users |
User search by name / username (min 2 chars) |
GET / PUT /discovery/pin |
Read / set the saved area pill |
GET /discovery/taxonomy |
The place + event category vocabulary |
GET /discovery/taxonomy/resolve |
Parse a free-text query into taxonomy terms |
GET /discovery/profiles/:id |
Full aggregated profile card (optional auth) |
The map-card detail reads for a single event or place render in the maps surface, and full event detail lives in Events. Discovery owns the location detail card and the aggregated profile card.
Search reuses the shared taxonomy parser plus the semantic resolver for natural-language intent, and the location geocoder to anchor a query on a named city (for example "bars in Toronto" from Ottawa).
Zoom tiers (the map)¶
| Tier | Zoom range | Shape returned | Sponsorship gate |
|---|---|---|---|
| Global | ≤ 4 |
Grid cluster aggregates, ~10° cells | — |
| Regional | 5–7 |
Grid cluster aggregates, ~3° cells | — |
| City | 8–10 |
Individual markers, caps 200/200/100 (events/locations/moments) |
sponsorship_tier > 2 filtered out |
| Neighbourhood | 11–13 |
Individual markers, caps 500/300/150 |
All tiers visible |
| Street | ≥ 14 |
Individual markers, caps 1000/500/200 |
All tiers visible |
At Global and Regional the server emits one geo_cluster marker per
cell with per-type counts; no individual event/location/moment rows
are returned. At City and above the server returns raw rows and the
client clusters them visually. Friend markers bypass the cluster path
at every zoom: they are always individual IsPriority = true markers.
Discoverable objects¶
| Object | When it appears |
|---|---|
| Event | Status in (upcoming / ongoing); visibility honoured (see Events) |
| Location | Always (real-world places are public) |
| Moment | Not yet expired (expires_at > now or journaled); visibility honoured |
| Friend marker | Friend is online AND has is_location_shared = true AND has live coordinates |
| Stranger (radar) | Within radius AND both the requester and the stranger have near_you_enabled AND the stranger has is_location_shared = true |
Visibility enforcement¶
Discovery introduces no new visibility rules. It enforces the rule each source domain already defines.
| Read | Rule |
|---|---|
GET /discovery/profiles/:id |
Unauthenticated callers get the public-only view; friends-visibility moments and events appear only to an accepted friend |
GET /discovery/locations/:id |
Moment highlights include friends-visibility moments only when the requester is a friend of the author; friend moments are sorted first |
GET /discovery/radar |
A stranger surfaces only when both sides opted in via near_you_enabled and the stranger is actively sharing location |
GET /discovery/map |
Event and moment markers honour the owning domain's visibility mode |
The profile card exposes a friendship_status of none,
pending_sent, pending_received, or accepted, computed relative
to the caller.
Invariants¶
| Invariant | Why |
|---|---|
| Discovery owns no domain state | It is a projection; the truth lives in the source domains |
| Low zoom always clusters | Server-side aggregate; no per-marker query at continent scale |
| Friend markers are never clustered | The whole point of the map is "where are my friends" |
| City zoom filters hyper-local sponsors | sponsorship_tier > 2 events are dropped at zoom ≤ 10 |
| The profile card degrades, never 404s on privacy | An unauthenticated or non-friend caller gets the public subset, not an error |
| A cancelled map fetch is a 499, not a 500 | Panning aborts in-flight requests; that is normal client behavior |
Cross-domain interactions¶
| Other domain | Reads |
|---|---|
| Events | Coordinates, status, visibility, sponsorship tier; host / joined / past events for the profile card |
| Locations | Coordinates, category, hours JSON, photos; the geocoder anchors search on a named city |
| Moments | Coordinates, visibility, expiry, like state; recent moments for the profile card |
| Friends | Accepted friendships for the map overlay and radar; mutual counts and friendship_status for the profile card |
| Presence | Live coordinates and online flags via a single Redis MGET per batch |
| Identity | Public profile fields; the lean user card is a separate identity-owned read |
See also¶
- Narrative: Bible → Discovery
- Backend: Backend → Discovery service
- Related spec: Identity → the profile split
- UX:
../ux/discovery.md(coming soon)