---
phase: 12-native-tablo-chat
plan: 03
subsystem: discussion
tags: [chat, discussion, sse, realtime, htmx, templ]

requires:
  - phase: 12-native-tablo-chat
    plan: 01
    provides: discussion message persistence and POST send
  - phase: 12-native-tablo-chat
    plan: 02
    provides: persistent read state and unread badge behavior
provides:
  - Authenticated owner-only SSE stream endpoint
  - In-process discussion event broker
  - Local EventSource browser integration
  - Duplicate suppression between sender POST response and SSE event
  - Composer reset after successful sends
affects: [discussion, router, static-assets, templates, tests]

tech-stack:
  added: []
  patterns:
    - "Realtime receive uses SSE from the Go process; sends remain CSRF-protected HTMX POSTs."
    - "Discussion SSE events carry rendered message HTML with message IDs for duplicate suppression."
    - "Composer reset is attached directly to the HTMX form after successful requests."

key-files:
  created:
    - backend/internal/web/discussion_broker.go
    - backend/static/discussion-sse.js
  modified:
    - backend/internal/web/handlers_discussion.go
    - backend/internal/web/handlers_discussion_test.go
    - backend/internal/web/router.go
    - backend/cmd/web/main.go
    - backend/templates/discussion.templ
    - backend/templates/discussion_forms.go
    - backend/templates/layout.templ

key-decisions:
  - "Use in-process pub/sub for the first native realtime slice; Postgres remains the reconnect/missed-event source of truth."
  - "Do not add WebSockets, managed realtime providers, workers, or runtime CDN scripts."
  - "Keep sender POST rendering, but suppress duplicate rows when SSE wins the race."
  - "Use `hx-on::after-request` on the composer form for clearing input, avoiding fragile delegated HTMX event source lookup."

patterns-established:
  - "Discussion routes accept `DiscussionDeps{Queries, Realtime}`; router provides a default broker when none is injected."
  - "SSE handlers set `text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`, and `X-Accel-Buffering: no`."
  - "Local JS silently reconnects through EventSource and exposes no connection-status, typing, or presence UI."

requirements-completed: [CHAT-04, CHAT-05]

duration: ~45min
completed: 2026-05-16
---

# Phase 12 Plan 03: Discussion Realtime Summary

**Native realtime discussion delivery is implemented with Go-owned SSE receive and HTMX POST send.**

## Performance

- **Duration:** ~45 min
- **Started:** 2026-05-16T08:16:00Z
- **Completed:** 2026-05-16T08:33:05Z
- **Tasks:** 4
- **Files modified:** 9

## Accomplishments

- Added RED tests for stream authentication, owner-only access, SSE headers, broker publishing, broker cancellation cleanup, and browser script behavior.
- Added `DiscussionBroker` with per-tablo subscriptions, non-blocking publish, and unregister-on-cancel behavior.
- Added protected `GET /tablos/{id}/discussion/stream` with SSE headers, initial flush, keepalives, and request-context cancellation.
- Wired discussion POST to broadcast newly inserted messages after DB persistence and read-state update.
- Added local `/static/discussion-sse.js` EventSource integration with duplicate suppression.
- Added form-level HTMX reset behavior so successful sends clear the composer reliably.
- Completed browser UAT: one visible row per sent message, composer clears, second tab receives messages without manual refresh, and no external realtime provider is used.

## Task Commits

1. **Task 1: Add RED SSE handler and broker tests** - `c6dcb68`
2. **Task 2: Implement SSE broker, stream route, and local browser glue** - `d15c374`
3. **Realtime UAT fixes: duplicate suppression and composer reset** - `6f17c30`, `426d89c`, `1034efc`, `409245e`

## Files Created/Modified

- `backend/internal/web/discussion_broker.go` - Adds in-process discussion pub/sub.
- `backend/internal/web/handlers_discussion.go` - Adds stream handler and publish-after-insert behavior.
- `backend/internal/web/handlers_discussion_test.go` - Adds SSE, broker, and browser-script regression coverage.
- `backend/internal/web/router.go` - Adds discussion stream route and default broker wiring.
- `backend/cmd/web/main.go` - Wires production discussion broker.
- `backend/templates/discussion.templ` - Adds stream URL hook and form-level reset behavior.
- `backend/templates/discussion_forms.go` - Adds stream URL helper.
- `backend/templates/layout.templ` - Loads local discussion SSE script.
- `backend/static/discussion-sse.js` - Adds EventSource append and duplicate suppression.

## Decisions Made

- The broker broadcasts persisted messages only after the database insert succeeds.
- The browser script does not surface connection state; EventSource handles silent reconnect.
- Sender duplicate prevention stays client-side because the same user may legitimately have a second tab that should receive the SSE event.
- Composer reset is attached to the HTMX form instead of a delegated script handler after HTMX v2 event detail differences made delegated source lookup brittle.

## Deviations from Plan

- UAT found two UI regressions in the initial SSE implementation: sender-visible duplicate rows and composer text not clearing. Both were fixed before close-out.

## Issues Encountered

- HTMX v2 event details did not expose the source form where the initial delegated reset handler expected it.
- Canceling duplicate swaps meant `detail.successful` was not reliable for reset behavior, so reset moved onto the form with `hx-on::after-request`.

## User Setup Required

None.

## Verification

- RED command failed before implementation because SSE types/routes did not exist:
  `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./internal/web -run 'TestDiscussionStream|TestDiscussionBroker|TestDiscussionPostBroadcasts' -count=1`
- `cd backend && just generate` passed.
- `cd backend && TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./internal/web -run 'TestDiscussionStream|TestDiscussionBroker|TestDiscussionPostBroadcasts|TestDiscussion' -count=1` passed.
- `cd backend && TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1` passed.
- `git diff --check` passed.
- Browser UAT approved by user on 2026-05-16 after duplicate-row and composer-reset fixes.

## Self-Check: PASSED

- CHAT-04 is covered by SSE implementation and browser UAT.
- CHAT-05 is satisfied with only Xtablo-owned Go/Postgres/static infrastructure.
- No WebSocket, managed chat, external realtime runtime, or runtime CDN script was added.
- Stream auth, ownership, headers, broker publish, and cancellation cleanup have automated coverage.

## Phase 12 Readiness

Phase 12 is ready for final phase verification and close-out.

---
*Phase: 12-native-tablo-chat*
*Completed: 2026-05-16*
