2.**One stream per page.** A page asks for its topics on one
50
connection: `GET /live?topics=...`, `text/event-stream`. The browser
51
reconnects by itself; `Last-Event-ID` resumes, and the server replays
52
what was missed from a short buffer, or says "reload" when it cannot.
53
3.**The page fetches what changed.** An event says which part of the
54
page is stale. The page fetches that part as HTML from the normal
55
route (`?fragment=comments`, `?fragment=checks` and so on) and
56
swaps it in. So:
57
- the templates that render the page render the update;
58
- permission is checked once, by the route that renders the
59
fragment; an event reaching the wrong viewer reveals nothing but a
60
topic and a version;
61
- fragments can be cached and are tested like pages.
62
4.**Subscribing is checked too.**`/live` only accepts topics the viewer
63
may read (a private repository's topics need read access), so even
64
the fact that something changed does not leak.
65
5.**The page still works without it.** No JavaScript or no connection:
66
the page is what it is today, and a reload shows everything. Pending
67
work shows a placeholder ("Claude is reviewing…") that the stream
68
replaces.
69
6.**A small client, ours.** About 2 KB in `hub.js`: elements carry
70
`data-live="<topic>"` and `data-fragment="<name>"`; an event for the
71
topic refetches the fragment and swaps it, keeping focus, scroll and
72
anything being typed. No framework.
73
74
##5. At scale, across services
75
76
When the hub is split into services:
77
78
-**Services publish events to a durable log.** NATS JetStream is the
79
choice: small, self-hosted, clustered, with replay, so a sovereign site
80
runs it on its own machines. Postgres `LISTEN/NOTIFY` is an acceptable
81
first step for a single database.
82
-**A stateless live gateway** holds the browsers' connections. It
83
checks a viewer's subscription (asking the owning service, cached
84
briefly), relays topic events, and replays from the log for
85
`Last-Event-ID`. It never renders and never reads repositories, so it
86
scales out on its own; losing an instance means browsers reconnect.
87
-**Fragments stay with the service that owns the data** (issues,
88
proposals, Flow, Diffusion). Moving a page to a new service does not
89
touch the live layer.
90
-**The resolver's rooms** become topics on the log, so people on
91
different instances see each other.
92
-**Later:** WebTransport where streams need to be bidirectional and
93
many; SSE stays the default.
94
95
##6. Order
96
97
| Step | What | Issue |
98
|---|---|---|
99
| L1 | Event bus inside the hub (broadcast per topic, versions, a short replay buffer); publish from the writes that notify and webhooks already see | #203 |
100
| L2 |`GET /live?topics=` with subscription checks, `Last-Event-ID` replay, and the `hub.js` client with `data-live` / `data-fragment`| #204 |
101
| L3 | The proposal page live: comments, reviews, assistant answers and pending placeholders, checks, landing state | #205 |
102
| L4 | Dashboard and inbox live, with the notification count in the top bar | #206 |
103
| L5 | Issue page, Diffusion tab and repository overview live | #207 |
104
| L6 | Fold the Flow run stream and the resolver's rooms into the same bus | #208 |
105
| L7 | Services: JetStream behind the bus and the live gateway as its own service | #209 |