page.corvus.backchannelFrame

Wire format for messages a client sends server-bound over the same WebSocket opened by `page.corvus.subscribeOps`. Atproto's `subscription` lexicon type is server-push only — every server→client frame is defined under `page.corvus.subscribeOps`. The client→server side of the duplex socket is described here as a separate frame union. Clients dispatch on each message's `$type` discriminator (e.g. `page.corvus.backchannelFrame#op`). **Multi-block.** A single WebSocket connection authenticates one DID and carries subscriptions to a dynamic set of blocks. After upgrade the connection has no active subscriptions; clients send `#subscribe { blockId, cursor? }` frames to start receiving ops for a block and `#unsubscribe { blockId }` to stop. No descent — each frame names exactly one block id; to follow a tree, the client reads child ids out of materialized data and sends another `#subscribe`. **Why a backchannel.** Live typing wants the lowest possible latency. Rather than POST every keystroke, ops flow client→server on the open WebSocket as `#op` frames. The server appends each submitted op to the op log, assigns a server-side `cursor`, and fans the op out to every subscribed connection (including the publisher) as a `page.corvus.subscribeOps#op` frame. **Client-side echo.** The same client also writes each op as a `page.corvus.edit` record into its own PDS via DPoP OAuth — WS-first then PDS, so peers see ops via the relay before the author's PDS is durably updated. The server does NOT write to the author's PDS on their behalf. The server's jetstream subscription is the backstop for ops the client wrote to PDS but never sent over the socket; opId dedup makes the two ingress paths interchangeable. **Ack via echo.** There is no explicit `#ack` frame — every submitted op is broadcast back to the same WebSocket as a `page.corvus.subscribeOps#op` frame carrying the server-assigned `cursor` and `blockId`. Publishers identify their own ops by `op.id` and treat the echo as confirmation. Same-op redeliveries (network retries, or arrival via jetstream after a WS submit) dedupe at the server's opId primary key and re-echo with the existing cursor, so the client's match-by-opId logic is naturally idempotent. **One DID per connection.** The WebSocket upgrade carries authentication identifying a single DID. Every `#op` on this connection must carry an op whose author is that DID; mismatches are rejected with a `page.corvus.subscribeOps#error` frame carrying `code: 'AuthorMismatch'` and the offending `opId`. **ACL.** The authed DID's effective role under `page.corvus.aclGrant` (resolved against the most-specific scope on the parent chain of every block the op touches) is enforced at `#op` submission time: - `write` → accepted as authored. - `suggest` → accepted; the server forces `op.suggestion = true` unless the op targets a `#comment` block. - no effective `write`/`suggest` grant → rejected with `#error { code: 'Forbidden', opId }` on the subscription. Project roots (named in their DID docs) are implicit `write` + `grant` at the project root scope.

Repository @corvus.page did:plc:3qc4cbzcriye72qqqodeda26 namespace authority
Details · defs
Kinddefs
Authoritycorvus.page
Indexed

What's different

There is nothing to compare yet: this is the only version of page.corvus.backchannelFrame anywhere.