Skip to content

fix(notifications): broadcast live comments to non-member viewers - #1057

Draft
leahpeker wants to merge 3 commits into
mainfrom
fix-1043-nonmember-comment-broadcast
Draft

fix(notifications): broadcast live comments to non-member viewers#1057
leahpeker wants to merge 3 commits into
mainfrom
fix-1043-nonmember-comment-broadcast

Conversation

@leahpeker

@leahpeker leahpeker commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Overview

Non-member (RSVP-token) viewers of a public event page could see comments but never saw new ones appear live, while logged-in members did. The frontend's only SSE subscription lived in NotificationBell, which is authed-only ({isAuthed ? <NotificationBell /> : null}), so a non-member never opened a stream connection at all — even though the backend's broadcast_event_comment_update() already pings a wildcard ("*") channel that any connected viewer, member or not, is designed to receive.

Root cause: frontend never attempted the SSE subscription for non-authed viewers. A secondary blocker: even if it tried, POST /api/notifications/sse-ticket/ (which mints the ticket EventSource needs, since it can't send an Authorization header) was auth=gated_jwt — a non-member has no JWT, so ticket-minting itself would 401.

Changes

  • SseTicket.user is now nullable; SseTicket.mint_anonymous() mints a ticket with no user attached.
  • POST /sse-ticket/ now uses auth=_optional_jwt (existing optional-JWT auth already used elsewhere for public endpoints) — mints a bound ticket for a logged-in caller, an anonymous one otherwise. Anonymous minting is rate-limited by IP.
  • notification_stream / _sse_generator / _format_notify_for_user accept user_id: str | None. Wildcard event_updated broadcasts still deliver to an anonymous connection; the personal notification channel never matches one (an anonymous viewer has no personal notifications to receive).
  • EventCommentsCard now opens its own SSE subscription when the viewer isn't authed, invalidating only that event's event-comments query key on event_updated. Authed members are unaffected — they still get this via NotificationBell's existing subscription, so no duplicate connection is opened for them.

Closes #1043

Test plan

  • make agent-typecheck (backend ty check) — pass
  • make agent-lint (backend ruff) — pass
  • make agent-frontend-typecheck (tsc -b) — pass
  • make agent-frontend-lint (eslint) — pass
  • backend/tests/test_sse.py run directly against a real per-worktree Postgres DB (SSE/pg_notify needs real Postgres, not sqlite) — 15/15 pass, including new tests for anonymous ticket minting and wildcard delivery to an anonymous connection
  • Manual browser verification against a local dev server (per-worktree Postgres, uvicorn without --reload — the reload file-watcher was found to interrupt long-lived SSE connections in dev, unrelated to this fix): one browser context authenticated as a member, a separate context logged out and RSVP'd anonymously via the public RSVP form to unlock the comments card as a non-member. Confirmed via network trace the anonymous viewer's POST /sse-ticket/ and GET /stream/?ticket=... both succeeded (200), then posted a comment as the member — the anonymous viewer's comment list updated live with no manual reload or refresh.

…sue 1043)

Non-member/anonymous viewers of a public event page never received live
comment updates, since the frontend only opened an SSE subscription from
NotificationBell, which is authed-only. The backend broadcast (wildcard
event_updates ping) already reached any connected viewer.

- SseTicket.user becomes nullable; anonymous viewers mint a ticket too via
  a new /sse-ticket/ optional-auth path, rate-limited by IP when anon.
- notification_stream/_sse_generator accept a null user_id: wildcard
  event_updated frames still deliver, personal notification frames don't.
- EventCommentsCard opens its own SSE subscription when not authed, scoped
  to invalidate only that event's comment list.
Tighten multi-line comments/docstrings to single lines per repo comment
style; drop redundant issue-number test docstrings.
- cap concurrent anonymous SSE connections (per-worker, cache-backed
  counter) so removing the auth requirement on ticket minting doesn't
  leave connection concurrency fully unbounded
- reuse existing auth_or_ip_key for the ticket-mint rate limit instead
  of a hand-rolled key that skipped proxy-aware IP resolution
- drop the redundant User row fetch in _consume_ticket (on_delete is
  CASCADE, so the DoesNotExist branch was unreachable); return the
  user_id string directly instead of routing through a sentinel
- note in EventCommentsCard why its SSE subscription intentionally
  no-ops for authed users
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

new comment broadcast

1 participant