page.corvus.subscribeOps
Subscribe to CRDT op streams from a corvus server over a single WebSocket connection. **Connection-scoped, multi-block**: one connection authenticates one DID and may carry subscriptions to many block sets. Subscribes and unsubscribes happen mid-connection via `page.corvus.backchannelFrame#subscribe` / `#unsubscribe` — the upgrade itself takes no parameters and starts with no active subscriptions. **Subscriptions name an explicit block set.** A subscribe frame carries the `blockIds` the client wants ops for. The server does NOT descend into children, references, or any other graph relation — the client decides which blocks to watch and adds/removes from the set as it discovers (or stops caring about) new ids. Sharing a block across projects is just sharing its id across subscriptions. **Same socket, bidirectional.** Atproto's `subscription` lexicon type only describes server→client messages. Client→server frames (subscribes, op submits, cursor advances) are defined in `page.corvus.backchannelFrame` and flow over the same WebSocket. **Per-op cursor.** The server stamps every op with a monotonic `cursor` as it ingests (whether the op arrived via `#opSubmit`, `page.corvus.submitOps`, or jetstream from a member PDS). The cursor is strictly increasing per server, and is NOT op identity (use `page.corvus.core#opId` for that). Clients track the highest cursor they've seen *per block* to drive resume; the wire `cursor` parameter on `#subscribe` is the floor across all subscribed blocks. **Cursor / resume.** `#subscribe` carries an optional `cursor`; the server replays every op whose cursor is strictly greater than that value, scoped to the subscribed `blockIds`, before transitioning to live tail. Omitting `cursor` starts from live tail without replay. After reconnect, a client either re-subscribes with `cursor: <min lastCursor across blocks>` or sends a `#cursor` backchannel frame to advance. **Server role: app view + relay, not source of truth.** Edit records live in the **author's** PDS — clients write them via DPoP OAuth themselves (client-side echo, WS-first then PDS). The server ingests via two paths: live `#opSubmit` on the WS (fastest peer relay) and jetstream from each subscribed DID's PDS (backstop, catches ops the client wrote to its PDS but never sent over the socket). The server dedupes by opId across both paths and assigns a single cursor per op. The server's op log + materialized snapshot are caches over PDS-stored truth; backfill from PDSes can rebuild them. **Echo as ack.** A client's own submitted ops are echoed back as `#op` frames carrying the server-assigned `cursor` and `blockId`. Publishers recognize their own by `op.id` ending in their authed DID. There is no separate ack frame. **Per-op errors.** Rejections of a specific `#opSubmit` (`AuthorMismatch`, `Forbidden`, `MalformedSubmit`) flow back as `#error` frames carrying the offending `opId` AND `blockId`. Subscribe-level errors (`UnknownBlock`, `Forbidden`) carry `blockId` and omit `opId`. Connection-level errors omit both. **Auth.** The WebSocket upgrade carries authentication identifying one DID; that DID is the author of every `#opSubmit` on this connection and the subject of ACL resolution on each block subscribed. The auth mechanism is described separately and may evolve.
Details · subscription
| Kind | subscription |
|---|---|
| Authority | corvus.page |
| Indexed |
What's different
There is nothing to compare yet: this is the only version of page.corvus.subscribeOps anywhere.