Part 1 — Resource Taxonomy & Module Layout
Tags reflect resources, and Go modules get split to match (the deferred cohesion audit, now
in scope). The current organization module is the main offender — it owns org CRUD + space/room
CRUD + (via the membership handler) invitations. We separate those.
Target resources → owning module → tag
| Resource (tag) | Owning Go module (target) | Current location | Move |
|---|---|---|---|
| Recordings | recording | recording + start/stop in room/livekit wiring | consolidate: bring start/stop under /recordings |
| Spaces | space (new, split from organization) | organization handler RoomCreate/Delete/Update/List/ByID/ByName | extract module (N1: Space) |
| Realtime | realtime / room | room handler RoomAuth (/livekit/room-auth), realtime token | transport tokens — A/V conference token (LiveKit today, vendor off the wire) + Centrifugo connection token (shared by chat & presence) |
| Invitations | invitation (new, split from organization/membership) | membership handler under /organizations/* | extract module |
| Members | membership | membership handler ListMembers/UpdateRole/RemoveMember | shrink to members only |
| Organizations | organization | organization handler Create/Update/Switch | shrink to org-only |
| Calendar | calendar/google | same | de-Google paths; {provider} selector for Outlook |
| Auth / Session | authentication + session | same | path cleanup |
| Chat | chat | same | messaging only (connection token → Realtime, presence → Presence) |
| Presence (user-status) | presence (split from chat) | Centrifugo status: channels | cross-cutting — heartbeat + manual status + snapshot; consumed by space/members/chat |
| User / Avatar | user | user (profile, avatar, signups) | avatar = sub-resource of user |
| Analytics / Metrics / Webhooks | analytics / metrics / webhook | same | likely stay plain gin (internal/webhook, signature-verified) — not part of the public typed contract |
Naming decisions to resolve (N)
N1 — "Office" vs "Room" vs "Space" (the triple-naming) — RESOLVED: Space (2026-06-15)
Canonical wire term = Space. Originally locked as Office (2026-06-13, matching the "virtual office"
framing + the FE module), then reopened and re-decided 2026-06-15 when the product broadened beyond work
offices: "office" is too narrow — a space's backdrop can be a lecture hall, lounge, or event floor, and those
are just SpaceLayouts, not different entities. Space is the neutral category term for spatial
collaboration (also Gather's core noun). Resource path /spaces, tag Spaces, DTO Space, sub-resources
SpaceLayout/SpaceTag. The DB table stays private.rooms internal (behind the repository); only the wire
term and the new module (space) change. See resources/space.md.
Renamed in lockstep so the three layers don't collide:
- Container →
Space(was Office on the wire / room internally). - Open zone "open space" → "open area" (it was an office-ism; "space" now names the container).
- Enclosed zone stays "meeting room"; the A/V session stays the vendor-neutral
conference-token. - Field name resolved: any wire field holding a space id is
spaceId(wasroomId).
Implied FE refactor (separate, large): officeLayoutStore, assets/layouts, the virtual-office
module / office feature, RoomData/roomId, and the literal "open space" term → Space / open-area
vocabulary. Tracked as its own migration; not part of the contract drafting.
N2 — Recording lives in two namespaces today
/livekit/start-recording, /livekit/stop-recording (room module) vs /recordings/list,
/recordings/download-link, /recordings/update-presence (recording module). Target: all under
/recordings, all owned by the recording module. Final paths (per resources/recording.md, which is
authoritative — every path ends in a reserved verb per C1/C1b, never a bare POST /recordings):
POST /recordings/start·POST /recordings/{recordingId}/stop·POST /recordings/list·POST /recordings/{recordingId}/download-link·POST /recordings/presence/update.
N3 — LiveKit "room-auth" / join — LOCKED: Realtime resource (2026-06-13)
/livekit/room-auth mints a token to join a space's realtime A/V session. Two framings:
- (a) Keep as a
Realtimeresource — mint a token artifact. - (b) Model as a space action:
POST /spaces/{id}/join→ returns the token. Decided: (a) — keep the token op in therealtimemodule; the realtime session is an infrastructure concern, not a space CRUD action. Final path isPOST /realtime/conference-token(perresources/realtime.md) — a verbless artifact-noun terminal, the sanctioned C1b*-tokenform. The vendor name (livekit) is not on the wire (C6) — it's the A/V conference token; LiveKit is today's transport.
N4 — Invitations as their own resource
send / list / revoke / accept move from /organizations/* to /invitations. Final paths (per
resources/invitations.md; every path ends in a reserved verb — no bare POST /invitations, no literal
:get colon, which C1a/C1b forbid):
POST /invitations/send(body = emails) ·POST /invitations/list·POST /invitations/{id}/revoke·POST /invitations/accept(body = token) · invite-link:POST /invitations/link/get/POST /invitations/link/reset/POST /invitations/link/extend.
N5 — Webhooks & internal ops stay out of the typed contract
/webhook, /webhook/license-expired, /webhook/reconcile-recordings, /generate-pilot-invite,
/health, /metrics — signature-verified or ops-internal. LOCKED (2026-06-13): keep as plain
gin handlers (migration plan §"public webhooks" open item), not Huma operations, so they don't
pollute the published client contract. Confirm per-route during their resource passes.
Module-split work implied (backend)
These are real code moves, sequenced with their resource passes — not a separate big-bang:
- Extract
spacemodule fromorganization(handlers, service, models, routes, DI). - Extract
invitationmodule fromorganization/membership. - Consolidate recording start/stop out of the room/livekit wiring into
recording. organizationandmembershipshrink to their true responsibilities.
Each extraction lands with that resource's blueprint pass so the move + reshape happen together, reviewed as one coherent unit.