Discovery¶
Purpose¶
DiscoveryService is the read-only aggregate that powers the map, the radar, the public profile, and the detail panels for events / locations / moments. It speaks PostGIS for spatial queries and Redis (via RedisService) for the live friend overlay. RecommendationService is its scoring sidekick — it ranks events by a simple category-affinity + distance-decay model and is invoked by EventService.GetEvents rather than by handlers directly.
The two services together implement the viewport tier strategy that keeps the map fast at any zoom level (continent-wide cluster aggregates at low zoom, individual markers at high zoom).
Responsibilities¶
DiscoveryService¶
GetDiscoveryData— bounding-box + zoom-tier map data. Tiers 0-1 (zoom ≤7) return grid-aggregatedgeo_clustermarkers; tiers 2-4 return individual events / locations / momentsGetRadarData— friends + Near-You strangers sorted by distance, both filtered by Haversine radiusGetLocationDetail— location metadata, top moment highlights (friends-first), upcoming events at that locationGetEventDetail— full event payload with visibility / invite-only enforcement;chat_room_idonly returned to joined participantsGetMomentDetail— full moment with privacy + expiry enforcement; like state for the requesterGetUserProfile— public profile with mutual-friend counts, recent moments, upcoming + past events. Works for unauthenticated requesters (degraded view)SearchUsers— name/username ILIKE search with batch friendship-status lookup and optional mutual-friend enrichment
RecommendationService¶
RankEvents(userID, events, params)— orders an event slice bycategoryAffinity * 10 + distanceDecay * 20. Returns events unchanged on profile-build error so it never breaks the listing.
HTTP endpoints¶
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/discovery/map |
JWT | Viewport map data (min_lat, max_lat, min_lng, max_lng, zoom) |
| GET | /api/v1/discovery/radar |
JWT | Nearby users (lat, lng, radius in metres) |
| GET | /api/v1/discovery/location/:id |
JWT | Location detail |
| GET | /api/v1/discovery/event/:id |
JWT | Event detail |
| GET | /api/v1/discovery/moment/:id |
JWT | Moment detail |
| GET | /api/v1/discovery/user/:id |
optional | Public user profile (auth optional) |
| GET | /api/v1/profiles/:id |
none | Same as above, public alias for sharing |
| GET | /api/v1/users/:userId |
JWT | Same handler, behind auth |
| GET | /api/v1/users/search |
JWT | User search (q, showMutual) |
Key types¶
type DiscoveryService struct {
db *gorm.DB
rdb RedisService
}
type RecommendationParams struct {
Location *models.Spatial
MaxDistanceKM float64
}
type RecommendationService interface {
RankEvents(ctx context.Context, userID string, events []models.Event, params RecommendationParams) ([]models.Event, error)
}
type UserProfile struct {
CategoryAffinity map[string]float64 // keyed by event category
}
Zoom-tier strategy¶
flowchart TD
A[GetDiscoveryData] --> B{zoom}
B -->|<=4 Global| C[Grid clusters, 10° cells]
B -->|5-7 Regional| D[Grid clusters, 3° cells]
B -->|8-10 City| E[Individual markers, 200/200/100 caps, sponsor filter]
B -->|11-13 Nbhd| F[Individual markers, 500/300/150 caps]
B -->|>=14 Street| G[Individual markers, 1000/500/200 caps]
C --> H[appendFriendMarkers]
D --> H
E --> H
F --> H
G --> H
H --> I[DiscoveryPayload]
At tiers 0-1 a single UNION SQL query snaps every coordinate to a grid cell, groups by cell, and returns one cluster marker per cell with event / location / moment counts. The frontend renders these directly without re-clustering. At tiers 2-4 the frontend's Supercluster library handles visual grouping; the backend sends raw rows.
Friend markers are always individual IsPriority=true markers at every zoom level — they bypass the cluster path so the user can always see exactly where friends are.
Data model¶
events—coordinates(PostGIS),status,sponsorship_tier,visibility,location_id,host_idlocations—coordinates(PostGIS),category,moment_count(joined),average_rating, photos / hours JSON, soft-delete viadeleted_atmoments—coordinates(PostGIS),visibility,expires_at,is_journaled,location_id,likes_count,media_urlfriendships— read directly to enumerate friends and compute mutualsevent_participants— to flag the user's own events and compute participation state
Dependencies¶
*gorm.DB— rawST_Intersects/ST_X/ST_YPostGIS queriesRedisService—MGetagainstuser:location:,presence:,active_location:for friend / stranger overlaysrepository.EventRepository(viaRecommendationService) — user's participation history for category-affinity profileutils.TimezoneFromCoords(frombackend/internal/utils/) — applied to event payloads for client-side rendering
Notable behavior¶
Spatial uses raw SQL with ST_X/ST_Y
PostGIS geometries are scanned back as floats through ST_X(coordinates) AS lng, ST_Y(coordinates) AS lat. Don't try to map the geometry column directly — GORM cannot decode the binary representation and will silently drop rows.
Visibility / privacy enforcement
GetEventDetailrejects invite-only events unless the requester has anapprovedparticipant rowGetMomentDetailrejectsfriends_onlymoments from non-friendschat_room_idon an event detail is only echoed back to joined participants / hostsfriends_onlymoments / events appear on a user's profile only if the requester is an accepted friend
Sponsor priority
Events with sponsorship_tier = 1 (global) bypass Supercluster on the frontend by being marked IsPriority=true. At city zoom (tiers ≤10), events with sponsorship_tier > 2 are filtered out so the global view doesn't drown sparse viewports with hyper-local sponsors. The user's own hosted / joined events are also IsPriority=true regardless of sponsorship.
H3 indexing
H3 cells are computed by LocationService at write time (github.com/uber/h3-go/v4, resolution 12 ≈ 5m radius) and stored on locations.h3_index. Discovery doesn't recompute them — it relies on them for fast nearby-location dedup when a moment or event is being created or resolved.
Where to look¶
backend/internal/services/discovery_service.gobackend/internal/services/recommendation_service.gobackend/internal/handlers/discovery_handler.gobackend/internal/dto/—DiscoveryRequest,RadarRequest, response payloads (MapMarker,LocationDetailPayload, etc.)backend/internal/models/spatial.go— PostGIS helpers