# Realtime browser messages Centrifugo with Protobuf is the browser update transport. Contexts publish typed messages to an explicit domain or security audience. The API calculates every channel from the authenticated principal's current authority. Browsers neither name nor select channels. Run applications have a separate [Project-private serving gateway](docs/decisions/0069-project-private-run-services.md). It forwards application HTTP, SSE and WebSocket traffic with continuously checked member authority. Platform projections and notifications use the rules in this document. ## Audiences and permissions `DeliveryTarget` permits one audience: | Target | Purpose | Channel derived by the gateway | | --- | --- | --- | | Project ID | State shared with connections currently authorised for a Project | `pharos:project:` | | Party ID | State shared with a Party | `pharos:party:` | | Principal subject | Private asynchronous outcome for one authenticated subject | `pharos:principal:` | Project and Party are domain audiences. Principal is the authenticated security subject used for private messages, including recorded command rejections and a per-principal projection. A Run is Project-owned, so Run state uses the Project target. Its payload remains a typed `execution_core/run` message with the Run ID and revision. SSH Access writes one principal-targeted publication for every actor that acts as the member. The connect proxy authenticates the session and asks an application outbound port for the principal's current authorised realtime audiences. The API adaptor composes published owner queries from Party Governance and Project: Party Governance maps the security principal to the Party authority it may exercise, and Project decides which Projects that Party may currently access. The API maps the returned canonical IDs to server-side Centrifugo subscriptions. It does not reproduce either context's access rules. The principal target is included for ordinary authorised connections. A limited API-owned session awaiting AUP receives only its own Party readiness channel. Invalid or unavailable session/Party authority fails closed. The connection carries a server-only audience fingerprint. Refresh asks the owners again and expires the connection when that fingerprint changes; the client reconnects to receive the new subscription set. Limited sessions use the canonical Party already revalidated by authentication. The registered Party payload is currently readiness only; adding other Party projections requires reviewing limited-session delivery authority. Once AUP is current, `party.Audiences` asks `GetCurrentParty` and traverses `ListPartyProjectAudiences`, whose Project owner checks `runs.view` for the exact API Party actor and authentication generation. A page may contain no allowed Projects and still supply a continuation cursor. Refresh retains its one-minute maximum and is capped by the earliest absolute `valid_until` returned by Beto. Expired, malformed or incomplete authority closes the connection. Development operator/CLI sessions without canonical Party authority retain only their principal channel. The connect grant starts before owner reads to preserve the logout fence. If those reads exhaust its lifetime, the API refuses the attempt with retryable disconnect code `4001`; a fresh attempt checks authority again. Invalid sessions and unresolved audiences retain terminal code `4501`. Expired grants supply no subscriptions. The [dashboard navigation qualification](docs/qualification/dashboard-navigation-990.md) records the regression and live authentication results. ## Publishing The aggregate transaction records its current state, receipt, outgoing message bytes and dispatch intent atomically. Each message supplies a `RealtimeTarget` and a context-owned typed snapshot. The relay sends only the encoded publication and its idempotency key: ```json {"b64data":"","idempotency_key":""} ``` The restricted gateway authenticates the publisher, verifies the envelope owner and permitted target kinds, derives the channel and forwards the original bytes to Centrifugo. Publisher code has no channel parameter. Execution Core may publish Project-targeted Run messages. Support publishes principal-targeted Ticket and TicketMessage messages, each to its owner. Party Governance publishes `PartyProfileProjection` to the owning principal, including private profiles. Profile data never uses the Party readiness channel available to limited sessions. The API records technical session-readiness notifications atomically in its owned Valkey state; its session credential can publish only the typed Party-targeted notification described in the [authentication guide](services/api/adaptors/authentication/README.md). Beto and Themelio expose the same typed `RealtimeMessage`/`RealtimeTarget` application API; Support and Party Governance explicitly enable their context-owned browser delivery in composition. Metallo constructs Run changes with `application.realtime.run_change`, which selects `RealtimeTarget.project(run.project_id)`. Beto publishers use `application.ProjectTarget`, `PartyTarget` or `PrincipalTarget`. Themelio uses the corresponding `RealtimeTarget` class methods. Rejection messages require a principal target in both technical foundations. SSH Access records its typed projection with every committed root revision, including Site results and propagation completion. Its actor mapping creates one principal-targeted envelope per actor; a member with no actor receives no publication. The `ssh_access` publisher credential is accepted only for principal targets. ## Frontend API The new web application owns the connection in `RealtimeProvider`. Feature code observes connection state and typed messages through hooks: ```tsx const connection = useRealtimeConnection(); useRealtimeMessage(realtimeMessageTypes.runChanged, message => { applyIfNewer(message.run.runId, message.revision, message.run); }); ``` `useRealtimeMessage` registers a local typed handler. It does not subscribe to a Centrifugo channel. `messages.ts` decodes the common envelope and its registered payload, checks the target against payload identity and rejects unknown contracts. The checked-in protobufjs schema is generated from the Protobuf sources by `pnpm generate:contracts`. The retained web keeps its Run connection in the authenticated `RunNoticeProvider`. It observes every server-authorised Project while the member navigates between pages. Resource managers select their Project's refresh counter; consent notices remain one per Run for its initiating member, including on the dashboard. Replacing the canonical member closes the old connection and clears its notices. An authorised current-state read remains the initial source of truth. A feature applies only newer revisions and tolerates duplicate delivery. Recoverable Centrifugo history resumes by epoch and offset. An unrecoverable gap requires one fresh owner query before later messages are applied. The SSH Access view registers a recovery callback with `RealtimeProvider`. The principal channel's first subscription reads `GET /api/v1/ssh-access/sites/{siteId}/projection`; recovered history needs no second read, and loss of history triggers one reconciliation. A view mounted after the principal subscription is ready receives its initial read immediately. Views mounted during an outage read once on the next subscription, including when existing views resume from recovered history. Browser HTTP reads use the generated OpenAPI client. The initial Project directory is loaded once after session establishment; it is not a polling source or a substitute for an owner-authorised realtime audience. ## Security rules - Client publish, client-selected subscriptions, polling, SSE and raw WebSocket business transports are prohibited. The one exception is the Run log panel, which polls its logs route every 5 seconds for now ([decision](services/metallo/docs/epics/instantiate-and-run/run-logs.md#browser-polling-for-the-log-panel)). - Publisher credentials are context-scoped and never reach the browser. - The gateway forwards exact persisted Protobuf bytes; it does not reconstruct a later snapshot. - Realtime delivery carries committed state or an outcome. It grants no business authority. - Session revocation disconnects every connection for that principal. Connect authority is short-lived and refreshed through the authenticated API proxy. The browser edge serves `/auth`, `/api/v1` and `/connection/websocket` at `https://platform.localtest.me`. Internal proxy, diagnostics and Centrifugo administration endpoints remain private.