docs(19.1): capture phase context

This commit is contained in:
2026-10-09 20:10:21 +02:00
parent b7649581da
commit cbb8f4c6f5
2 changed files with 352 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
# Phase 19.1: Chat satellites - Context
**Gathered:** 2026-10-09
**Status:** Ready for planning
<domain>
## Phase Boundary
The five remaining shared plugins that Wavepath's app plugin requires run on SummerCMS, each in its own `sm-*-plugin` repo under `git.golem15.com/golem15/`:
| PHP plugin | Go repo / plugin ID | What it is | PHP size |
|---|---|---|---|
| `Golem15.ChatBots` | `sm-chatbots-plugin` / `golem15.chatbots` | Slash-command registry and dispatcher over chat, the `bot_flags` table, the `BotAuthResolver` seam | ~1.4k LOC |
| `Golem15.ChatVideo` | `sm-chatvideo-plugin` / `golem15.chatvideo` | LiveKit video sessions per conversation, call links (`join_code`), `video:*` realtime events | ~1.1k LOC |
| `Golem15.Forum` | `sm-forum-plugin` / `golem15.forum` | Context-scoped categories, threads and posts in Markdown, read marks, a deny-all access resolver | ~2.1k LOC |
| `Golem15.Herald` | `sm-herald-plugin` / `golem15.herald` | Web Push delivery core: subscriptions, payload, send engine, 404/410 pruning, `HeraldProfile` and `PresenceGate` seams | ~1.5k LOC |
| `Golem15.UserFriends` | `sm-userfriends-plugin` / `golem15.userfriends` | Friendships, friend groups (members, invites, bans), the friends chat context adapter | ~2.1k LOC |
Each plugin provides its schema, models, a typed Go service API, a generic headless JSON API (D-05) and its events.
Out of scope:
- Wavepath's own `/_wave/api/v1/community/...` and `/_wave/api/v1/push/...` routes, its controllers, and its host bindings: `WaveForumAccessResolver`, `WaveBotAuthResolver`, `WaveHeraldProfile`, `WavePresenceGate`, `WaveNotificationDispatcher`, `CommunityVideoService` and the group-lifecycle wiring in `wavepath/Plugin.php`.
- The Vue frontend and the Wavepath app port itself.
**Repos touched:**
- The five new plugin repos.
- `sm-user-plugin`, only for additive user-lifecycle events that Phase 19 has not already added.
- `summercms.go`, for the framework Markdown module (D-17, delivered as a separate quick task), and only if `lighthouse`, `conga`, `fetchguard` or `festival` lack something.
**Roadmap corrections** (make them in the first planning commit):
- Success criterion 2: drop "Chatbots is wired to `sm-golem-plugin` where the PHP original calls Golem". The PHP chatbots plugin never calls Golem; it requires only Chat and User.
- Success criterion 2: herald's decision note names `webpush-go` (D-09). Add decision notes for LiveKit (D-13) and bluemonday (D-16).
- Success criterion 3 is already met. Fonoteka's and Wavepath's userfriends are the same commit (`182b514`, no diff), fonoteka's PHP code never references it, and fonoteka.go has no port. Wavepath's copy is the single source.
- "Repos" line: add `summercms.go` for the Markdown module.
</domain>
<decisions>
## Implementation Decisions
### Carried forward from Phase 19 (not re-decided)
- **D-01:** **The frozen port contract is Wavepath's tree.**
- The commits are chatbots `1ecf8c8`, chatvideo `25135b4`, forum `ad10b54`, herald `a749f83` and userfriends `182b514`, read from `/media/nvme/dev/jz/wavepath.org/plugins/golem15/`.
- The P15/P18/P19 frozen-source rule applies. These names and shapes are binding: tables and columns, migration-equivalent schema, realtime event names, channels and payloads, serializer output, console command names, config keys, env names and lang keys.
- **Reversibility:** costly. The names are copied into the database and clients of every app.
- **D-02:** **One squashed, idempotent baseline per plugin,** equal to the final PHP schema, that adopts an existing Wavepath database unchanged (Phase 5, P18.1 D-04, P19 D-04). The researcher diffs each baseline against a real Wavepath database.
- The demo-seed migrations (chatvideo `seed_demo_users`, userfriends `seed_demo_friends`) are not ported as behavior (D-07). Their version IDs stay recorded so that adopted databases stay consistent, and fresh databases get no demo users.
- **Reversibility:** one-way. Migration IDs and table shapes become the shared contract.
- **D-03:** **The P19 seam rules apply to every satellite.**
- Contexts register through the build-time morph registry (P19 D-11). Stored PHP class names in morph columns are rewritten to aliases on adoption (P19 D-12). That covers `golem15_forum_categories.context_type` and any satellite morph column the researcher finds.
- Cross-plugin hooks are typed `festival` events, and no plugin patches another's model (P19 D-13). The GDPR user-delete cascades listen to `sm-user-plugin` events:
- chatbots deletes the `bot_flags` row;
- forum sets `user_id` to null on threads and posts and keeps the bodies;
- userfriends hands group ownership to the oldest admin, else the oldest member, else hard-deletes the empty group, then removes the user's member and ban rows.
- Every access seam is deny-all by default and has a setter, and Boot logs a Warn while the default is active (P19 D-14). That covers `ForumAccessResolver`, `BotAuthResolver` and herald's `PresenceGate`.
- Realtime goes through `lighthouse`, and user-targeted pushes go to the `user` namespace that `sm-user-plugin` registers (P19 D-16, D-17).
- Nothing names QuestStream, family, parent or child (P19 D-02), and READMEs never name a consuming app.
- **D-04:** **A test-only host per plugin repo** (P18 D-03, P19 D-21): testcontainers Postgres, the lighthouse memory driver, fakes for external services (a LiveKit Twirp fake and a push-service fake), neutral `acme` fixture contexts, and `sm-user-plugin`, plus `sm-chat-plugin` where the plugin requires it. Every PHP test method maps to a Go case or to an explicit "not applicable" note. Golden payloads and events are captured from Wavepath. The mapping is completed in the last plan.
### HTTP surface
- **D-05:** **Every satellite ships a typed Go service API plus a generic headless JSON API under its own prefix,** following P19 D-06 to D-09:
- `/_forum/api/v1` mirrors `/_wave/api/v1/community/forums/{groupId}/...` (categories with create, rename, reorder and delete; threads, posts, edit and delete, read marks, unread count), re-rooted on context alias and id: `/_forum/api/v1/contexts/{type}/{id}/categories`. Access is decided by `ForumAccessResolver`.
- `/_chatvideo/api/v1` mirrors `/_wave/api/v1/community/groups/{id}/video` (start, join, leave, end, status) and `community/call-links/{code}`, addressed by conversation, or by context through chat's context registry.
- `/_userfriends/api/v1` mirrors `/_wave/api/v1/community/friends*` (list, requests, sent, request, accept, reject, cancel, unfriend) and the group routes (`community/groups*`: groups, members, invites, bans).
- `/_herald/api/v1/push/subscription` with POST, PATCH and DELETE, plus `GET /_herald/api/v1/push/vapid-public-key`, mirroring `/_wave/api/v1/push/*`.
- `/_chatbots/api/v1` covers the command catalog, filtered by `isAvailable`, and command dispatch, mirroring Wavepath's `CommunityCommandController`. The invoker is always the authenticated actor (PHP CMD-06).
- Auth is `jwt.auth`, mounted with the `lighthouse.Mount` surfaces pattern so the app picks the guard. PHP's named rate buckets carry over: `video-initiate` at 5 per minute per user, the friend-request bucket (`community-join`), and a general bucket per API. The actor id always comes from the token, never from the request body.
- One serializer set serves both the generic API and the future `/_wave` aliases, so payloads are identical.
- **Reversibility:** one-way. These are published APIs that apps will consume.
- **D-06:** **This reverses the PHP "herald ships no routes" rule** (herald README, D-78-04) for consistency with the other plugins. The herald README's reuse contract is rewritten around the generic API and the Go seams.
- **D-07:** **Dropped, as in P19 D-10:**
- the legacy plugin-owned routes `/api/video/*` and `/api/friends/*`, which no client calls;
- the WinterCMS components `VideoCall`, `FriendsList` and `FriendRequests` and their templates;
- chatvideo's bundled `livekit-client.umd.min.js`, `video-chat.js`, `video-vue.js` and CSS;
- the demo seeders (D-02).
- **D-08:** **Satellites couple through generic context events, never through each other's types.**
- userfriends fires typed festival events for group created, group deleted, member joined and member left.
- forum exposes a generic `DeleteContext(type, id)`, a soft-delete cascade over categories, threads and posts. It does not import userfriends.
- The app, or a small optional bridge in the app, wires group-deleted to `forum.DeleteContext`. This replaces PHP forum's `class_exists(FriendGroup)` hook.
- The other group hooks in `wavepath/Plugin.php` (spawning a chat or channel on group create, ending video on group delete) stay app wiring for the Wavepath port.
- userfriends registers its chat context adapter (the `FriendContext` provider) softly, only when chat is registered, through an optional-capability check rather than a hard requirement. This keeps PHP's "chat is the lower layer" rule.
### Herald (Web Push)
- **D-09:** **Web Push uses `github.com/SherClockHolmes/webpush-go`,** which covers VAPID (RFC 8292) and aes128gcm (RFC 8291).
- The decision note required by the roadmap goes in `.planning/notes/` before any herald code lands. It records the version, maintenance state, transitive dependencies (it should already share `golang-jwt/jwt/v5`), and how its HTTP client is swapped for fetchguard's (D-10).
- The researcher verifies that the library's `Options.HTTPClient` accepts a `fetchguard.Client`-backed doer. If it doesn't, the note records the adapter.
- **Reversibility:** reversible. It sits behind the plugin's own sender.
- **D-10:** **Every push send goes through a `fetchguard` PublicOnly client** (HTTPS only, private and reserved IPs refused at dial time).
- There is no `EndpointGuard` seam. Dial-time checks close the subscribe-then-send DNS-rebind gap that PHP's IP pinning (`CURLOPT_RESOLVE`) and the host-bound `SsrfGuard` existed for.
- Subscribe time also validates the endpoint URL (HTTPS, public host).
- This is fail-closed by default, where PHP's `AllowAllEndpointGuard` was fail-open.
- **D-11:** **Sends run as a `conga` job, one per send.**
- `SendEvent` and `SendUnconditional` enqueue a River job inside the caller's transaction, so a rolled-back write pushes nothing.
- The job runs `PresenceGate.ShouldSend` (for `SendEvent` only), builds the payload with `HeraldProfile`, sends to every subscription the user has, and prunes 404 and 410 endpoints.
- River provides retries and backoff. PHP's rate-limited OBS-01 warning for other delivery failures carries over.
- The PHP defaults carry over: TTL 2419200 (4 weeks) and urgency `normal`, both overridable per send.
- **D-12:** **VAPID keys are config keys** `golem15.herald.vapid.{subject,public_key,private_key}` with the default subject `mailto:admin@example.com`.
- The PHP env names `VAPID_SUBJECT`, `VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY` are mapped so an adopted `.env` works unchanged.
- `summer herald:vapid-generate` (the PHP `herald:vapid-generate`) prints a new keypair. The private key is never logged and never appears in a payload.
- Boot warns when the keys are missing. Keys are never generated automatically, because rotating them invalidates every subscription.
### ChatVideo (LiveKit)
- **D-13:** **chatvideo uses the official `github.com/livekit/server-sdk-go`** for access tokens and RoomService (create, delete, remove participant, participant count, list rooms).
- A decision note in `.planning/notes/`, written before the code lands, records the version, the transitive dependency footprint (`livekit/protocol`, twirp, protobuf; flag anything heavy such as pion), and how tests fake it.
- The token claims must match PHP exactly, verified by a golden test: HS256, `iss` set to the API key, `sub` set to the user id as a string, `name`, `iat`/`nbf`/`exp` with `token_ttl` defaulting to 900, a `video` grant with `room`, `roomJoin`, `canPublish` and `canSubscribe`, and an optional JSON `metadata` string carrying the avatar.
- The admin URL keeps PHP's fallback: `livekit_admin_url`, else `livekit_url` with `wss://` or `ws://` rewritten to `https://` or `http://`. An error for a missing room is swallowed as in PHP (`participantCount` returns 0, `deleteRoom` is a no-op).
- Config keys and env names carry over: `livekit_url` and `LIVEKIT_URL`, the API key, the secret, `max_duration` (3600), `token_ttl` (900), `livekit_admin_url`, `max_participants` (8) and `screen_share`.
- **Reversibility:** reversible. It sits behind the plugin's LiveKit service.
- **D-14:** **The stale-session reconciler moves from the Wavepath app into chatvideo.**
- A generic reconcile service ends sessions whose LiveKit room is gone or empty, or that have run past `max_duration`. It is exposed as `summer chatvideo:reconcile-sessions` and as a `conga` periodic job (every 5 minutes, as Wavepath schedules it).
- The Wavepath port's `wave:reconcile-video-sessions` becomes a thin alias. The researcher reads `wavepath/console/ReconcileVideoSessions.php` and keeps only its generic part.
- **D-15:** **Realtime events are the PHP events, published through lighthouse.**
- `video:call:incoming` goes to recipients' `user:{id}` channels.
- `video:call:accepted`, `declined`, `ended`, `video:participant:joined` and `left` go to `video:session:{id}`.
- chatvideo registers the `video` namespace authorizer. It allows `video:session:{id}` only to participants of the session's conversation and grants capability `["prs"]`. It replaces `VideoChannelAuthorizer`.
- A publish failure is logged and not raised, as in PHP.
- The one-active-session-per-conversation unique index and the `card_message_id` and `join_code` columns carry over.
### Forum (Markdown)
- **D-16:** **Sanitizing uses `github.com/microcosm-cc/bluemonday`,** recorded in a decision note.
- The policy is the PHP HTMLPurifier config, 1:1:
- Allowed tags: `p, br, strong, em, b, i, h1-h6, ul, ol, li, blockquote, pre, code, a[href|rel|target], table, thead, tbody, tr, th, td, hr, del, ins, sub, sup`.
- URL schemes: `http`, `https` and `mailto`.
- Links get `target=_blank` and `rel=nofollow`.
- Conversion uses `goldmark`, already a dependency, with GFM. That covers tables, strikethrough and autolinks (the external-link behavior), with raw HTML input stripped (PHP `html_input=strip`) and unsafe links disallowed.
- **D-17:** **The renderer is a framework module, not forum code.**
- It is a small shared package in `summercms.go` (or an extension of cabana's markdown support; the quick task decides) that exports `Render(markdown)` and `Purify(html)` with a named policy.
- The user's direction: "Framework module, done with gsd-quick by summer agent. We'll need that in multiple places." It is delivered as a separate `/msd-quick` task in `summercms.go` by the summer agent. It is not a plan of this phase. The forum plans consume it, and the Wavepath port's `purify()` callers (trainings) use it too.
- If the quick task hasn't landed when forum execution starts, the forum plan blocks on it. The forum plan does not grow its own renderer.
- The module's README and `docs/` page follow the Documentation rules in CLAUDE.md.
- **D-18:** **`content_html` parity is semantic, and stored rows are kept.**
- Adopted `content_html` rows are served as stored and never re-rendered on read. New and edited posts render in Go, on write, as in PHP.
- Golden tests compare a normalized DOM (the same tags, attributes and text) against PHP output for a corpus that covers the PHP `ForumMarkdownRenderTest` XSS cases (`<script>`, `javascript:`, `<iframe>`, `onerror`), GFM tables, code blocks, links and autolinks.
- Byte-identical HTML is not required.
### Claude's Discretion
- Go package, type and identifier names for the services, seams, events and jobs. Exact generic API paths within D-05.
- How chatbots resolves bot bearer tokens through `BotAuthResolver` (deny-all default) on the generic API, and where `is_bot` and the bot flag show up in serializers. Follow Wavepath's `BotRestReachTest` and `WaveBotAuthResolverTest` for the expected behavior.
- How chatvideo addresses a call in the generic API (by conversation id, or by context alias through chat's registry), as long as Wavepath's group-video payloads are reproducible.
- Queue names and job shapes on `conga` (herald send, video reconcile).
- Lang strings port to phrasebook in EN and PL. Plugin display names are neutral.
- Whether the D-12-style alias rewrite for satellite morph columns extends chat's adoption command or each plugin ships its own.
- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Expect userfriends and forum early (no external services), then chatbots, then herald and chatvideo (new dependencies and their decision notes).
- Run the security-review agent. The phase adds public authenticated APIs, an outbound push sender, a token issuer for a media server, an HTML sanitizer boundary and GDPR delete cascades.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Port sources (frozen, D-01)
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/`: `Plugin.php`, `contracts/{SlashCommand,BotAuthResolver}.php`, `classes/{SlashCommandRegistry,CommandDispatcher,CommandContext,CommandResult}.php`, `models/BotFlag.php`, `tests/unit/` (7 files)
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/`: `Plugin.php`, `classes/{LiveKitService,VideoCallNotifier,VideoChannelAuthorizer}.php`, `controllers/api/VideoApiController.php`, `config/config.php`, `models/`, `updates/` (v1.0.0 to v1.0.4)
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/`: `Plugin.php`, `classes/{ForumService,markdown/MarkdownRenderer}.php`, `contracts/ForumAccessResolver.php`, `models/`, `updates/` (v1.0.0 to v1.1.0), `tests/` (including `security/ForumMarkdownRenderTest.php`)
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/`: `Plugin.php`, `README.md` (the reuse contract), `contracts/{HeraldProfile,PresenceGate,EndpointGuard}.php`, `classes/{PushSender,PayloadBuilder,services/SubscriptionService}.php`, `config/herald.php`, `console/GenerateVapidKeys.php`, `tests/unit/`
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/`: `Plugin.php` (soft chat registration and the GDPR cascade), `classes/` (GroupPolicy, GroupVisibilityGate, the services, FriendsContextAdapter), `models/`, `updates/`, `tests/unit/`
### How the host consumes them (route and payload shapes for D-05, host seams)
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/routes.php`: lines 438-549 (groups and friends), 623-629 (group video), 631+ (forums), 830-840 (push subscription and vapid key), plus the command routes
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/Plugin.php` lines 160-460: seam bindings (forum, chatbots, herald) and group lifecycle wiring (D-08)
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/`: `CommunityForumController`, `CommunityFriendsController`, `CommunityGroupController`, `CommunityVideoController`, `CommunityCommandController`, `BotRestController` (bot bearer-token reach), `PushSubscriptionController`
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/console/ReconcileVideoSessions.php`: the reconciler that D-14 generalizes
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/classes/{chatbots,notifications,forum,video}/`: the host implementations of the seams (reference only, not ported)
- `/media/nvme/dev/jz/wavepath.org/vue-wavepath-app/src/api/{friendsApi,videoApi}.ts` and the push subscription code: the client's view of the payloads
### Project decisions
- `.planning/phases/19-chat-and-chatchannels/19-CONTEXT.md`: D-02 (no QuestStream), D-04 (baseline), D-06 to D-10 (HTTP surface), D-11 to D-14 (seams), D-16 and D-17 (lighthouse and the `user` namespace), D-21 (test host)
- `.planning/notes/core-plugins-own-repos.md`: the `sm-*-plugin` repo, module, submodule and commit workflow
- `.planning/notes/core-plugin-port-inventory.md`: satellite sizes and port order
- `.planning/notes/plugin-layout-winter-directories.md`: the plugin directory layout
- `.planning/phases/18-payment-gateway/18-CONTEXT.md` D-03, D-06 and `.planning/phases/18.1-payment-gateway-subscriptions/18.1-CONTEXT.md` D-04, D-08: the test-host, `/_x/api/v1`, baseline and morph-registry precedents
### Framework modules
- `modules/lighthouse/README.md`: publisher, `Emit`/`Publish`, Registry/Authorizer (the `video` namespace), `Mount`, memory driver
- `modules/fetchguard/README.md`: `NewClient`, `PublicOnlyMode` for herald sends (D-10), `TrustedMode` for the operator-configured LiveKit URL
- `modules/conga/README.md`: River jobs and periodic jobs (D-11, D-14)
- `modules/festival/README.md`: typed events (D-03, D-08)
- `modules/cabana/field_markdown.go`: the existing goldmark usage, a starting point for the D-17 module
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `lighthouse`: replaces `Golem15.WebSockets` (`CentrifugoClient::publish` and `publishToUsers`, `AuthorizerRegistry`) for chatvideo and herald's presence source.
- `fetchguard`: a guarded `http.Client` that checks the IP at dial time; it makes herald's `EndpointGuard` and IP pinning unnecessary (D-10).
- `conga`: River jobs for herald sends and the video reconciler.
- `festival`: typed events for user-delete cascades and group lifecycle (D-03, D-08).
- `goldmark` v1.8.6 is already in `go.mod` (used by cabana). bluemonday, webpush-go and livekit server-sdk-go are new dependencies, each with a decision note.
- `golang-jwt/jwt/v5`: already decided; shared with webpush-go and possibly livekit.
- `sm-chat-plugin` (Phase 19): the context registry, conversations and participants that chatvideo, chatbots and userfriends build on.
### Established Patterns
- A deny-all seam plus a setter, with a Boot warning (P19 D-14), replaces PHP's `$this->app->bind(Contract, DenyAll…)`.
- Build-time registries replace singletons: `SlashCommandRegistry` (with the reserved `help` trigger, `registerBuiltin`, and duplicate rejection) becomes a Boot-time Go registry.
- `/_x/api/v1` per plugin, with one serializer set shared by the generic API and the app aliases.
- A squashed idempotent baseline that adopts the PHP database.
### Integration Points
- chatbots, chatvideo → `sm-chat-plugin` (hard requirement). userfriends → chat (soft, optional capability). forum and herald → no chat dependency.
- All five → `sm-user-plugin` (user model, `jwt.auth`, delete events). Herald in PHP required only Apparatus and kept its own `user_id`-keyed table; keep it free of user-model imports beyond the id.
- The app wires: `ForumAccessResolver`, `BotAuthResolver`, `HeraldProfile`, `PresenceGate`, its slash commands, and group-deleted → `forum.DeleteContext`.
</code_context>
<specifics>
## Specific Ideas
- On the Markdown module: "Framework module, done with gsd-quick by summer agent. We'll need that in multiple places."
- The user picked established libraries (webpush-go, livekit server-sdk-go, bluemonday) over hand-rolled stdlib code. Each still gets a decision note under the dependency rule.
- Generic APIs everywhere, including herald and chatbots, so no host has to rewrite controllers.
</specifics>
<deferred>
## Deferred Ideas
- The framework Markdown/sanitizer module (D-17): delivered by a separate `/msd-quick` task in `summercms.go`, not by this phase's plans.
- A `vue-community-layer` or similar frontend components for friends, forum, video and push: once a client other than Wavepath wants them.
- Wavepath's `/_wave` routes and host seam implementations: with the Wavepath app port.
### Reviewed Todos (not folded)
- The todo matcher returned only keyword noise (translate editor, cabana richeditor, faq port, mailblocker). None concern the satellites, so none were folded.
</deferred>
---
*Phase: 19.1-chat-satellites*
*Context gathered: 2026-10-09*

View File

@@ -0,0 +1,119 @@
# Phase 19.1: Chat satellites - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-10-09
**Phase:** 19.1-chat-satellites
**Areas discussed:** HTTP surface, Herald Web Push lib, ChatVideo LiveKit, Forum Markdown
Scouting facts presented before the questions:
- chatbots never calls Golem, so success criterion 2's `sm-golem-plugin` clause is vacuous.
- userfriends is the same commit (`182b514`) in fonoteka and Wavepath and is unused by fonoteka's code.
- No client calls the satellites' own routes; Wavepath's SPA uses only `/_wave/api/v1/...`.
---
## HTTP surface
| Option | Description | Selected |
|--------|-------------|----------|
| Yes, like Phase 19 | Generic `/_x/api/v1` for forum, chatvideo and userfriends, mirroring the `/_wave` community routes and re-rooted by context alias | ✓ |
| Service API only | Matches PHP; the app owns every route | |
| Mixed | Generic for userfriends and forum; video service-only | |
| Option | Description | Selected |
|--------|-------------|----------|
| Generic APIs for both | `/_herald/api/v1/push/*` and `/_chatbots/api/v1` commands | ✓ |
| Herald yes, chatbots no | Command dispatch stays in the app | |
| Neither | Keep the PHP shape | |
| Option | Description | Selected |
|--------|-------------|----------|
| Drop all | Legacy routes, components, assets and demo seeds dropped; seed migration IDs kept as no-ops | ✓ |
| Keep legacy routes as aliases | Mount `/api/video` and `/api/friends` as aliases | |
| Drop routes, keep seeds | Demo seeders become an opt-in command | |
| Option | Description | Selected |
|--------|-------------|----------|
| Generic context events | userfriends fires group events; forum exposes `DeleteContext`; the app wires them | ✓ |
| Forum listens directly | Optional capability check on userfriends | |
| You decide | | |
---
## Herald Web Push lib
| Option | Description | Selected |
|--------|-------------|----------|
| Hand-rolled, stdlib | crypto/ecdh, hkdf, AES-GCM, and golang-jwt for VAPID | |
| SherClockHolmes/webpush-go | The de-facto Go library | ✓ |
| You decide after research | | |
| Option | Description | Selected |
|--------|-------------|----------|
| fetchguard built in | PublicOnly client, no EndpointGuard seam, fail-closed | ✓ |
| Keep the seam, fetchguard default | A host can add a push-service allow-list | |
| Port as is | Allow-all default | |
| Option | Description | Selected |
|--------|-------------|----------|
| conga job per send | Enqueued in the caller's transaction; gate, send and prune in the job | ✓ |
| Synchronous like PHP | | |
| Both | | |
| Option | Description | Selected |
|--------|-------------|----------|
| Config keys + env, port the command | PHP env names mapped; `herald:vapid-generate`; warn when missing | ✓ |
| Generate on first boot | | |
---
## ChatVideo LiveKit
| Option | Description | Selected |
|--------|-------------|----------|
| Hand-rolled | golang-jwt tokens plus Twirp JSON over fetchguard | |
| livekit/server-sdk-go | The official SDK, with a decision note | ✓ |
| livekit/protocol auth only | | |
| Option | Description | Selected |
|--------|-------------|----------|
| Move into chatvideo | A generic reconcile service, command and conga periodic job | ✓ |
| Leave it in the app | | |
---
## Forum Markdown
| Option | Description | Selected |
|--------|-------------|----------|
| bluemonday | The PHP HTMLPurifier policy mapped 1:1; goldmark for conversion | ✓ |
| Hand-rolled on x/net/html | | |
| Framework-level sanitizer | | |
| Option | Description | Selected |
|--------|-------------|----------|
| Semantic parity, keep stored rows | Normalized-DOM golden tests; adopted rows served as stored | ✓ |
| Byte-identical | | |
| Re-render all on adoption | | |
| Option | Description | Selected |
|--------|-------------|----------|
| In sm-forum-plugin, exported | | |
| Framework module | | ✓ |
**User's choice:** "Framework module, done with gsd-quick by summer agent. We'll need that in multiple places."
**Notes:** The module is delivered by a separate quick task in summercms.go, not by a plan of this phase. Forum consumes it.
---
## Claude's Discretion
- Identifier names, exact generic API paths, chatbots bot-token handling on the API, how calls are addressed in the chatvideo API, conga queue and job shapes, phrasebook strings, where the satellite alias rewrite lives, and the plan split.
## Deferred Ideas
- The framework Markdown/sanitizer module, delivered by a separate quick task.
- Frontend components for friends, forum, video and push.
- Wavepath's `/_wave` routes and host seam implementations, with the Wavepath app port.