hydrant docs
hydrant docsfor hydrant 0.1.0
hydrant 0.1.0

hydrant implements a jetstream-compatible websocket subscription endpoint when compiled with the jetstream feature. it allows clients to consume atproto repository updates (commits, identity updates, and account state transitions) via a simplified, JSON-based format.

GET /subscribe

subscribe to the jetstream websocket stream.

query parameters

param type description
wantedCollections seq list of collection NSIDs to receive (e.g. app.bsky.feed.post). supports namespace wildcards (e.g. app.bsky.feed.*).
wantedDids seq list of DIDs to receive (e.g. did:plc:abc123xyz).
wantedEventTypes seq list of event types to receive: live or historical. if not specified, cursor replay returns both and the live tail only live (see live stream scope).
maxMessageSizeBytes integer filters out events whose serialized JSON size exceeds this value.
cursor integer unix microseconds timestamp (time_us) to replay historical events from. a cursor past the newest event starts at the live tail.
compress boolean if true (or if header Socket-Encoding contains zstd), compresses frames using zstd and sends them as binary websocket frames.
requireHello boolean if true, the socket will not stream any events until the client sends a hello / options_update message first.

in-stream options update

clients can dynamically modify filtering criteria (wantedCollections, wantedDids, maxMessageSizeBytes, and wantedEventTypes) without reconnecting by sending a text frame with the following JSON format:

{
  "type": "options_update",
  "payload": {
    "wantedCollections": ["app.bsky.feed.post", "app.bsky.like.*"],
    "wantedDids": ["did:plc:abc123xyz"],
    "maxMessageSizeBytes": 5000000,
    "wantedEventTypes": ["live"]
  }
}

event payload formats

all jetstream events are serialized as JSON objects containing:

  • did: string (did of the repository)
  • time_us: integer (unix microseconds timestamp)
  • kind: string ("commit", "identity", or "account")

commit

fired when a record is created, updated, or deleted:

{
  "did": "did:plc:abc123xyz",
  "time_us": 1716823456789012,
  "kind": "commit",
  "commit": {
    "rev": "3kpjxabc123",
    "operation": "create",
    "collection": "app.bsky.feed.post",
    "rkey": "3kpjxabc123",
    "record": {
      "text": "hello, world!",
      "$type": "app.bsky.feed.post",
      "createdAt": "2026-05-27T12:00:00.000Z"
    },
    "cid": "bafyreihy..."
  }
}

record keeps the keys in the order the record's CBOR has them, shorter keys first, rather than sorting them.

a redaction stops a body from being served once it completes, except in events already queued for a reader at that moment.

if hydrant is running in indexer mode with HYDRANT_ONLY_INDEX_LINKS=true, record content blocks are not persisted. consequently, record and cid may be omitted from commit payloads.

identity

fired when a did document or handle updates:

{
  "did": "did:plc:abc123xyz",
  "time_us": 1716823456789012,
  "kind": "identity",
  "identity": {
    "did": "did:plc:abc123xyz",
    "seq": 1234567,
    "time": "2026-05-27T12:00:00Z",
    "handle": "user.bsky.social"
  }
}

account

fired when a repository's status changes (active, deactivated, or deleted):

{
  "did": "did:plc:abc123xyz",
  "time_us": 1716823456789012,
  "kind": "account",
  "account": {
    "active": false,
    "did": "did:plc:abc123xyz",
    "seq": 1234567,
    "time": "2026-05-27T12:00:00Z",
    "status": "deactivated"
  }
}

additional details

  • live stream scope: by default, cursor replay returns both live firehose events (live: true) and historical backfill / sync replay events (live: false), interleaved in commit order, while the live tail carries only live events, even after the subscriber falls behind and catches up from the database. filter them with the wantedEventTypes query parameter. asking for historical explicitly also delivers backfills that finish while the socket is open, from the live tail.
  • ordering: events are delivered in commit order, which is also time_us order, so a client that reconnects with its last time_us as the cursor misses nothing the socket would have sent.
  • replay speed: a cursor replay runs at most HYDRANT_STREAM_REPLAY_SPEED times the bandwidth the live stream would take (10 by default), or HYDRANT_STREAM_REPLAY_FLOOR_BYTES a second (about 80 Mbit/s) when that's more. events your filters skip count a little too, since hydrant still reads them. the live tail never waits on it. a single trusted consumer that has to catch up fast (such as a service resuming from its saved cursor) can leave jetstream out of HYDRANT_THROTTLED_STREAMS to replay uncapped.
  • falling behind: a subscriber that falls behind the live broadcast catches up from the database at its own pace instead of being disconnected.
  • slow consumers: if a socket's send buffer remains full for longer than the configured timeout, the server sends a {"type":"error","error":"ConsumerTooSlow"} message and drops the connection.