Domains¶
The Tomoda backend is organized as vertical domain slices. Each domain is one Go package under backend/internal/services/<domain>/ that owns its full stack: persistence, business logic, HTTP surface, and route mounting. This page is the map: what each domain owns, how a slice is shaped, and how slices talk to each other without importing each other's internals.
For the reasoning behind this shape see Vertical domain slices; for the tables each domain owns see Data Model.
Anatomy of a slice¶
A domain package is a self-contained feature. The files are conventional:
| File | Role |
|---|---|
store.go |
Persistence. The only layer that touches the database, running the domain's sqlc-generated queries over pgx. Returns *models.X. |
service.go |
Business logic and orchestration across stores, caches, and other domains' ports. |
handler.go |
HTTP: bind and validate input, call the service, shape the response. Owns its response DTOs. |
routes.go |
Mounts the domain's endpoints onto a passed-in group. Imports no middleware. |
wire.go / deps.go |
Wire provider set for the slice; deps.go declares the ports it needs from other domains. |
*_test.go |
Colocated unit and mock-based tests. |
Constructors are New<Thing> (NewEventService, NewEventStore). Types are Store, Service, Handler. Multi-aggregate domains prefix files and types by aggregate: event_service.go / EventService, plan_store.go / PlanStore. The full object graph is composed in backend/internal/wiring, and the route tree is assembled in backend/internal/wiring/router.go.
The domain map¶
Twenty domains. Most expose an HTTP surface; content, passport, and gameengine are internal (consumed by other slices, no routes of their own).
| Domain | HTTP prefix | Owns |
|---|---|---|
auth |
/auth, /users/me/avatar, .well-known/webauthn |
Identity, login, sessions, OTP, WebAuthn, API keys, avatar finalize. |
user |
/users/:userId (lean card), /users/me/* |
User card (GetUserCard), profile, preferences, denormalized stats. |
friend |
/friends, /location |
Friendship graph, close friends, friend location sharing. |
event |
/events, /share/events/:id, /users/:userId/events |
Event lifecycle, participants, event chat send, event-items snapshot, check-in. |
plan |
/plans, /items, /users/:userId/plans |
Plan canvas, item board (stash + shared), availability poll, collaborators, promotion. |
chat |
/chats, /ws/chats/:id, /ws/client |
Chat rooms, DMs, group chats, messages, reactions, the two WebSocket hubs. |
moment |
/moments |
Ephemeral, location-anchored posts, likes, companion tags. |
presence |
/presence, /checkins |
Heartbeat, opt-in live-location sharing, check-ins with companion tagging. |
location |
/locations/{nearby,nearby/autocomplete,reverse,resolve,countries,countries/:code,:id/report-outdated}, /admin/locations/ranking-weights |
Place search, geocoding, resolve, dedup, country reference, report-outdated, admin rank-weight apply. |
maps |
/map/location/:id, /map/event/:id, /map/* |
Map-card detail reads and per-user personalization (saves, dismissals, affinity). |
discovery |
/discovery |
Read-aggregation: viewport map, radar, search, location detail, pin, taxonomy, aggregated profile card. |
media |
/uploads, /klipy, /link-preview |
Presigned S3 upload starters, GIF/sticker proxy, SSRF-guarded link preview. |
notification |
/notifications, /push-tokens |
Inbox rows, grouping, push-token registration. |
partner |
/partners, /me/partners, /partner/:partner_id |
Multi-tenant partners and membership-scoped access. |
payment |
/payment/webhook, /auth/payment/* |
Stripe checkout, billing portal, webhook. |
admin |
/admin, /admin/async |
Admin console: stats, user moderation, location merge, async queue ops. |
content |
(internal) | Curated catalog: TomodaStamp, StampGuideEntry, Curio, Challenge. |
passport |
(internal) | Earned state: travel log, stamps, curios, progress, first-discoveries. |
gameengine |
(internal) | Rule evaluation: reads content, writes passport on qualifying actions. |
The taxonomy vocabulary, WebSocket hubs, Redis cache, S3 driver, email, audit, and the semantic LLM resolver are shared infrastructure in backend/internal/platform/* and backend/internal/storage, not domains. See System Overview.
Route composition¶
A domain's routes.go exposes RegisterRoutes(r, handler, ...gates) (where r is a chi.Router) and never imports the middleware or access packages. Auth gates (JWTAuth, OptionalJWTAuth, rate limiters, access.Require(...)) are constructed in backend/internal/wiring/router.go and passed in as func(http.Handler) http.Handler parameters. This keeps each slice free of a middleware dependency and makes the full gate-to-route mapping auditable in one file.
Public (optional-auth or unauthenticated) endpoints get a separate RegisterPublicRoutes: shareable event pages, the aggregated profile card, map-card reads, and provider webhooks.
Cross-domain ports¶
A slice never reaches into another slice's store or service. When domain A needs something from domain B, A declares a narrow interface (a port) in its own deps.go, and B's service satisfies it. The edge is one-way and mockable, and the wiring package supplies the concrete implementation.
deps.go; the provider implements it. The arrow is the port name, and it points from consumer to provider so import edges stay one-way.Concrete ports today:
| Consumer | Port (in its deps.go) |
Provider | What it fetches / does |
|---|---|---|---|
maps |
EventDetailProvider |
event |
Viewer-projected event detail for the map card. |
presence |
TagDMSender |
chat |
Deliver the companion-tag direct message. |
event |
MessagePurger |
chat |
Purge event-chat messages during lifecycle cleanup. |
passport |
ProfileDeltas, FirstDiscoveryClaimer |
user |
Fold a visit into denormalized profile stats. |
user |
FriendChecker, EventCleaner, AssetDeleter |
friend, event, media |
Friendship checks, orphan cleanup, async asset delete. |
The MessagePurger case shows why ports matter: chat depends on event's store, so event must not import chat. Instead event declares MessagePurger and chat implements it, keeping the dependency one-way.
Discovery is the read side¶
discovery is the query and read-aggregation domain. It queries the database directly across events, locations, users, and moments, and reuses taxonomy.ParseQuery, the platform/llm semantic resolver, and location geocoding. It serves the unified /discovery/search intent endpoint, the viewport /discovery/map, and the full aggregated profile card at /discovery/profiles/:id (optional auth).
Write-owning domains stay lean: user serves only the lean card at GET /users/:userId (GetUserCard → {id, name, username, avatar_url, bio, is_friend}); the rich, cross-domain profile is a discovery read. See Discovery is the read-aggregation domain.
Where to read next¶
- Per-domain implementation:
docs/backend/services/*.md. - The tables each domain owns: Data Model.
- Route mounting, middleware chain, process modes: System Overview and
backend/internal/wiring/router.go.