> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.graze.social/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Graze Turbostream

**Turbostream** is a live stream of new posts on Bluesky. It's built on Bluesky's Jetstream, but each post arrives with its context already filled in: the author's profile, the profiles of anyone mentioned, and the posts it replies to or quotes.

A raw Jetstream event only gives you identifiers (DIDs and AT-URIs). To make sense of it you'd normally look each one up through the Bluesky API, and cache the results. Turbostream does that work for you, so you can read the stream and use it straight away.

## Connecting

There's a single WebSocket endpoint and no setup:

`wss://api.graze.social/app/api/v1/turbostream/turbostream`

From the command line:

```sh
websocat "wss://api.graze.social/app/api/v1/turbostream/turbostream"
```

From JavaScript:

```js
const ws = new WebSocket("wss://api.graze.social/app/api/v1/turbostream/turbostream");
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.pong) return; // heartbeat
  console.log(msg.at_uri, msg.hydrated_metadata.user?.handle);
};
```

## What you receive

Each WebSocket message is **one JSON object**: a post, or a heartbeat.

### Posts

```json
{
  "at_uri": "at://did:plc:qipvtyo27owt4isjah3j3dw2/app.bsky.feed.post/3lng7e4mr5c2z",
  "did": "did:plc:qipvtyo27owt4isjah3j3dw2",
  "time_us": 1745342977546824,
  "message": { "did": "…", "time_us": 1745342977546824, "kind": "commit", "commit": { "…": "…" } },
  "hydrated_metadata": {
    "user": { "handle": "…", "displayName": "…", "followersCount": 0 },
    "mentions": {},
    "parent_post": null,
    "reply_post": null,
    "quote_post": null
  },
  "_id": "1745342977612-0"
}
```

- `at_uri`: the post's address.
- `did`: the author's DID.
- `time_us`: when Jetstream received the post, in microseconds.
- `message`: the original Jetstream event, unchanged. The post itself is in `message.commit.record`.
- `hydrated_metadata`: the context Graze filled in (see below).
- `_id`: this message's position in the stream.

### What's filled in

- `user`: the author's full profile, including handle, display name, avatar, bio and follower counts.
- `mentions`: everyone mentioned in the post, as a map from their DID to their profile.
- `parent_post`: the post being replied to, if the post is a reply.
- `reply_post`: the first post of the thread, if the post is a reply.
- `quote_post`: the quoted post, if the post quotes one.

Any of these is `null` (or, for `mentions`, empty) when it doesn't apply or couldn't be fetched in time. Always check before using them.

### Replays and heartbeats

- **On connect**, you first receive the 25 most recent posts, each marked `"_replay": true`. Then live posts follow.
- **Every 10 seconds**, you receive `{"pong": true}`. Use it to check the connection is alive.

You may see other fields in a message. Ignore any you don't recognise.

## No resume

Turbostream has no cursor. If you disconnect, any posts published while you were away are not sent when you reconnect; you only get the latest 25 again, as a replay. If you need every post, keep the connection healthy and reconnect quickly.

## Privacy

Accounts that have asked to be left out of Graze don't appear in Turbostream. Their posts aren't sent, and they're left out of `mentions`, `parent_post`, `reply_post` and `quote_post` on other people's posts.

## Tips for your client

- **Run a watchdog.** If nothing (not even a heartbeat) arrives for about 25 seconds, close the connection and reconnect. A connection can go silent without closing.
- **Reconnect with backoff.** If reconnecting fails, wait a little longer before each try.
- **Skip or handle replays.** If you count or compare posts, filter out `_replay` messages first, or you'll double-count the 25 you already saw.
- **Keep up.** Turbostream carries a lot of posts. Do slow work (database writes, API calls) off the receive loop, so you don't fall behind.
- **Read time from the event.** If `time_us` at the top level is `null`, use `message.time_us`.

## Looking for only some posts?

Turbostream sends every new post. If you only want posts that match a topic, build a feed in Graze and use [Contrails](https://www.graze.social/docs/contrails) to stream just its matches.