Web Contributor Map
Quick orientation for contributors changing the browser-facing web service.
Canonical implementation path: internal/services/web/.
Start Here
- Route ownership starts in
routepath/, arearoutes*.go, and areamodule.go. Keep routepath edits inside the owned surface file for the area you are changing instead of regrowing a cross-area path bucket. - Protected flow is usually
routes.go->handlers*.go->app/->gateway/. - Public flow is usually
routes.go->handlers.go->app/->gateway/, withpublichandler.Basefor shared rendering behavior. - Top-level startup and composition live outside feature areas:
cmd/web,internal/cmd/web, andinternal/services/web/{server.go,principal/,composition/,app/,modules/}. - Start orientation with
doc.goininternal/services/web/,internal/services/web/module/, andinternal/services/web/modules/before dropping into implementation files. - Startup dependency policy and dependency-to-config address ownership are defined in
internal/services/web/startup_dependencies.go. Command-layer connection lifecycle lives ininternal/cmd/web/dependency_graph.goandinternal/cmd/web/runtime_dependencies.go. - Service-owned dependency bundle construction lives in
internal/services/web/dependencies.go; do not patch partially-built bundles later inRun.
Package roles
internal/services/web/principal: request-scoped session validation, viewer chrome, locale resolution, grouped principal callbacks, and the middleware-owned principal snapshot. Start here when changing app-shell request resolution flow.internal/services/web/module: canonical module contract types only. Shared request-state callback contracts belong inprincipal, not here.internal/services/web/app: root mux composition, auth wrapping, and same-origin protections.internal/services/web/modules: registry builder plus module dependency bundles. Registry files call area-ownedCompose(...)entrypoints instead of constructing feature gateways inline, and shared runtime helpers such as dashboard sync are built here once and passed into owning areas.internal/services/web/modules/<area>: route owner for one feature area.internal/services/web/modules/<area>/app: area-local orchestration and input validation.internal/services/web/modules/<area>/gateway: backend protocol mapping.- For layered areas, the production flow is
composition.go-> ready app service(s) inConfig->module.go->handlers*.go.Mountshould not rebuild app services from raw gateways. internal/services/web/modules/<area>/render: area-owned render and view-model seams once a page set outgrows sharedtemplates/. Start withdoc.go, then exported entrypoints, not generated*_templ.go.internal/services/web/modules/<area>/workflow: transport-owned system-specific workflow contracts and implementations when an area has multiple workflow adapters. Start withdoc.go, then registry or service entrypoints, then system subpackages.internal/services/web/modules/notifications: inbox transport and view mapping stay area-owned, but the canonical notification payload contract lives ininternal/services/shared/notificationpayload.internal/services/web/platform/*: shared transport helpers only. Start with packagedoc.goand package-local tests before editing those seams.internal/services/web/templates: shared shell and layout primitives. If one area’s page set becomes a hotspot, move that set under the owning area instead of extending the shared package indefinitely.
For area-owned public surfaces that are optional by dependency, prefer ComposePublic-style constructors that return (module.Module, bool) from composition.go so the registry can explicitly include or omit whole routes based on configured clients. For protected surfaces, prefer ComposeProtected constructors that centralize shared options and dependency mapping in composition.go, then gate optional protected surfaces (like notifications) in the registry based on explicit configured checks.
Where changes usually belong
- New route or changed route contract: owning module
routes*.go,module.go, and the matching owned file inroutepath/. - Changed page behavior with the same backend shape: owning module handlers and view mapping first, then the area-owned render seam if one exists, and shared
templatesonly for shell-level primitives. - Changed web-only workflow or validation: owning module
app/. - Changed backend transport mapping or proto normalization: owning module
gateway/. - Shared auth, request, session, or page shell behavior:
principal,platform/, or root composition packages, but only after confirming it is truly cross-cutting.
Current hotspots
campaigns: still the largest area, but the root sink files are gone. Campaign workspace surfaces now live undercampaigns/{overview,participants,characters,sessions,invites}with shared workspace-shell support incampaigns/detail; the root package mainly owns module composition, starter/catalog transport, stable route-surface assembly, and system/workflow installation policy. Start withmodule.go, then the owned surface package orroutes_*.go, thenrender/doc.goorworkflow/doc.gowhen those seams are involved.settings: route/files and production composition now keep account and AI ownership split end to end. Start withcomposition.go, then the matching account-vs-AI handler, app, and gateway files.discovery,profile,invite,dashboard, andnotifications: all use the same small-module archetype wherecomposition.gobuilds the production gateway plus app service andmodule.goonly wires transport concerns.publicauth: continuation-path validation, signed-in detection, and page/session/passkey/recovery composition are all area-owned now. Start withcomposition.goand the specific capability files instead of looking for one transport-wide bundle.templates: shared shell/layout primitives only. Keep area-owned pages out of it.
Guardrails to trust
internal/services/web/modules/architecture_test.gointernal/services/web/modules/boundary_guardrails_test.gointernal/services/web/modules/constructor_guardrails_test.gointernal/services/web/routepath_guardrails_test.gointernal/services/web/templates/routes_guardrails_test.go- Per-module
routes_test.gofiles
These guardrails should protect package boundaries, constructor ownership, and route contracts. They should not require placeholder files or contributor-owned split names just to preserve a preferred filesystem shape.
High-signal coverage entrypoints
- Root runtime and shell behavior:
internal/services/web/server_test.gointernal/services/web/server_locale_test.gointernal/services/web/server_viewer_test.gointernal/services/web/server_static_test.go - Root test harness ownership and shared web test fixtures:
internal/services/web/server_test_harness_defaults_test.gointernal/services/web/server_test_harness_helpers_test.go - Startup dependency policy and runtime wiring:
internal/services/web/dependencies_test.gointernal/cmd/web/web_test.go - Shared request-state and transport helpers:
internal/services/web/principal/requeststate_test.gointernal/services/web/platform/modulehandler/modulehandler_test.gointernal/services/web/platform/publichandler/publichandler_test.gointernal/services/web/platform/pagerender/pagerender_test.gointernal/services/web/platform/weberror/weberror_test.gointernal/services/web/platform/dashboardsync/sync_test.go - Canonical test map:
docs/architecture/platform/web-testing-map.md
When changing boundaries, update docs and guardrails in the same slice so the next contributor inherits the new shape instead of reverse-engineering it.