Skip to content

Maps

Purpose

Maps serves the detail sheet a map marker opens and the per-user personalization that rides on it. Discovery emits the viewport markers; Maps serves the location and event cards those markers tap through to, plus the save / dismiss actions that shape a user's map over time. It reads the canonical Location, Moment, and Event rows directly.

Code lives in backend/internal/services/maps/.

HTTP surface

Routes are in backend/internal/services/maps/routes.go. The detail reads are public with optional auth (share links resolve unauthenticated; authed callers get viewer-relative fields like is_saved). The personalization actions require JWT and mount under the caller's /map group.

Method Path Auth Handler Description
GET /map/location/:id optional LocationDetail Full location card; is_saved set when authed
GET /map/event/:id optional EventDetail Full event card via the event domain
POST /map/location/:id/dismiss JWT DismissLocation Hide from my map, downshift affinity
POST /map/location/:id/save JWT SaveLocation Bookmark a place
DELETE /map/location/:id/save JWT UnsaveLocation Remove the bookmark

Cards

LocationDetail returns MapLocationDetailResponse (types.go): the place's identity and geo denorm, rating and save_count, opening hours, moment highlights, and upcoming events. is_saved reflects the requester's user_saves row when authenticated.

EventDetail does not build its own payload. It reuses the event domain through the EventDetailProvider interface (deps.go), calling event.GetEventDetail and returning event.EventDetailResponse so a map event pin and the event screen render from the identical canonical shape. Maps imports that response; it is not duplicated here.

Both handlers 404 on a non-UUID :id rather than binding-error, since the path is a share-facing surface.

Personalization

Save, unsave, and dismiss adjust two things in one transaction: the bookmark row and a category-affinity score used to bias what the map surfaces. All three are idempotent so double-taps do not inflate counts.

Action Row effect Affinity effect
Save Upsert user_saves, save_count += 1 user_affinity.score += 0.05 (capped at 1.0)
Unsave Delete user_saves, save_count -= 1 (floored at 0) score -= 0.03 (floored at 0)
Dismiss Record dismissal score -= 0.03 (floored at 0)

Affinity is keyed (user_id, category) and upserted via INSERT ... ON CONFLICT.

Key types

  • MapLocationDetailResponse, MapOpeningHours, MomentHighlightBrief, EventBrief, UserBrief in backend/internal/services/maps/types.go. The EventBrief / UserBrief briefs are the Maps-local copies of the cross-cutting shapes.
  • EventDetailResponse, owned by the event domain, imported.

Where to look