> ## Documentation Index
> Fetch the complete documentation index at: https://docs.member.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Realtime (SSE)

> Subscribe to live hub events with Server-Sent Events.

The realtime endpoint delivers live hub events to connected members over Server-Sent Events (SSE). Use it to drive badges, live discussion feeds, DM notifications, and online-presence indicators without polling.

## Connect

```http theme={null}
GET /realtime/events?hub_id={hub_id}
Authorization: Bearer <contact-access-token>
Accept: text/event-stream
```

The connection stays open indefinitely. The server sends events as they happen and a `:heartbeat` comment every 25 seconds to keep the TCP connection alive.

### Error responses before the stream opens

| Status | Cause                                                                         |
| ------ | ----------------------------------------------------------------------------- |
| `400`  | `Last-Event-ID` is present but is not a valid UUIDv7                          |
| `401`  | Token is missing, expired, or invalid                                         |
| `404`  | Hub does not exist, contact is not an active member, or contact is banned     |
| `429`  | Contact already has 5 open SSE connections (across all hubs on this instance) |
| `503`  | The server is draining (shutting down). Reconnect after a short delay.        |

## Event frame format

Each event follows the standard SSE wire format:

```
id: <UUIDv7 event id>
event: <event type>
data: <JSON payload>

```

The `id` field is a UUIDv7 string. Store it as `Last-Event-ID` for gap-free reconnects.

## Event types

All emitted event types are registered at startup. Current set (updated with each release):

### Community — spaces

| Type                             | When                            |
| -------------------------------- | ------------------------------- |
| `community.space.created`        | A space was created             |
| `community.space.updated`        | A space was updated             |
| `community.space.deleted`        | A space was deleted             |
| `community.space.member_joined`  | A member joined a space         |
| `community.space.member_left`    | A member left a space           |
| `community.space.member_muted`   | A member was muted in a space   |
| `community.space.member_unmuted` | A member was unmuted in a space |

### Community — discussions

| Type                                    | When                       |
| --------------------------------------- | -------------------------- |
| `community.discussion.published`        | A discussion was published |
| `community.discussion.updated`          | A discussion was updated   |
| `community.discussion.deleted`          | A discussion was deleted   |
| `community.discussion.locked`           | A discussion was locked    |
| `community.discussion.unlocked`         | A discussion was unlocked  |
| `community.discussion.pinned`           | A discussion was pinned    |
| `community.discussion.unpinned`         | A discussion was unpinned  |
| `community.discussion.reaction_added`   | A reaction was added       |
| `community.discussion.reaction_removed` | A reaction was removed     |

### Community — comments

| Type                                 | When                                  |
| ------------------------------------ | ------------------------------------- |
| `community.comment.created`          | A comment or reply was posted         |
| `community.comment.updated`          | A comment was edited                  |
| `community.comment.deleted`          | A comment was removed                 |
| `community.comment.reaction_added`   | A reaction was added to a comment     |
| `community.comment.reaction_removed` | A reaction was removed from a comment |

Payloads carry IDs only (for example `comment_id`, `discussion_id`, `contact_id`). Fetch the full resource from the REST API when you need body text or attributes.

### Community — notifications

| Type                             | When                                            |
| -------------------------------- | ----------------------------------------------- |
| `community.notification.created` | A new notification was created for this contact |

Use `community.notification.created` to update the unread badge in real time without polling `GET /api/v1/hubs/{hub_id}/notifications/unread-count`. The payload carries the notification's `id` and `hub_id`; fetch the full notification resource if you need its category, kind, or preview text.

### Community — profiles and DMs

| Type                                  | When                              |
| ------------------------------------- | --------------------------------- |
| `community.profile.updated`           | A community profile was updated   |
| `community.conversation.created`      | A new DM conversation was created |
| `community.conversation.member_added` | A member was added to a group DM  |
| `community.conversation.member_left`  | A member left a DM conversation   |
| `community.message.created`           | A DM message was sent             |
| `community.message.edited`            | A DM message was edited           |
| `community.message.deleted`           | A DM message was tombstoned       |
| `community.message.read`              | A DM message was marked read      |

### Broadcasts

| Type                            | When                                      |
| ------------------------------- | ----------------------------------------- |
| `community.broadcast.published` | A broadcast post was published to the hub |

### Test

| Type        | When                                           |
| ----------- | ---------------------------------------------- |
| `test.ping` | Synthetic ping for development and smoke tests |

## Payload shape

Every event's `data` is a JSON object. The exact fields vary by event type, but all payloads include identifiers (for example `discussion_id`, `space_id`, `contact_id`) so the client can look up the full resource if needed. Payloads intentionally carry IDs rather than full bodies to keep the event stream lean.

## Access scoping

Events are delivered based on the member's access at connect time:

* **Hub-scoped events** (spaces, directory) reach all connected members.
* **Space-scoped events** (discussions, reactions) are filtered: restricted spaces are only delivered to members whose segment grants access.
* **Direct-targeted events** (DMs, 1:1 presence) are delivered only to the named contact IDs.

If a member's access changes while they are connected (for example, they are removed from a restricted space), the server updates the connection's access set without requiring a reconnect.

## Terminal events

The server closes the stream by emitting a terminal event before dropping the TCP connection. Terminal events have no `id` field and are not resumable.

| Event type            | Cause                                                                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `stream.expired`      | The contact's JWT expired. Refresh the token and reconnect.                                                                                            |
| `stream.unauthorized` | The contact was banned or had access revoked mid-stream.                                                                                               |
| `stream.draining`     | The server pod is shutting down. The `data` includes a `retry_ms` hint; wait that many milliseconds before reconnecting.                               |
| `stream.stale_resume` | The `Last-Event-ID` cursor is older than the 5-minute retention window. The client must do a full page refresh to reconcile state before reconnecting. |

## Reconnect and gap-free resume

Store the most recent `id` value from received events. On reconnect, send it as `Last-Event-ID`:

```http theme={null}
GET /realtime/events?hub_id={hub_id}
Authorization: Bearer <contact-access-token>
Last-Event-ID: 018f4b3c-1234-7abc-8def-000000000001
```

The server replays all events emitted after that ID from the last 5 minutes, then switches to live delivery. If the cursor is older than 5 minutes the server emits `stream.stale_resume` — on receiving that event, perform a full page refresh to reload state, then reconnect without a `Last-Event-ID`.

`Last-Event-ID` must be a UUIDv7 string. Sending any other format returns `400`.

## Heartbeat

The server sends an SSE comment line every 25 seconds:

```
: heartbeat

```

This keeps the TCP connection alive through proxies. No action is needed on the client; SSE clients ignore comment lines. If neither an event nor a heartbeat is received for 90 seconds the server closes the connection — reconnect using the standard `Last-Event-ID` flow.

## Connection limit

A single contact may hold at most 5 concurrent SSE connections across all hubs. Opening a sixth connection returns `429`. Close unused connections (for example, when navigating away from a hub view) to stay within this limit.

## X-API-Version

Every SSE response (including the initial HTTP 200 that establishes the stream) includes the `X-API-Version` response header set to the running build version. See [X-API-Version](#x-api-version-header) below.

***

## X-API-Version header

Every response from the API — including SSE streams, webhooks, and preflight CORS responses — includes:

```http theme={null}
X-API-Version: 0.3.1
```

The value is the build version of the currently running instance (same as the `version` field from `GET /version`). Use it to:

* Confirm a deploy has completed and the expected version is live.
* Emit version metadata in client telemetry.
* Debug which build served a specific request when the `X-Request-ID` alone is not enough.

The header is set on the outermost middleware layer, so it is present even on responses that short-circuit before reaching any route handler (for example, CORS preflight responses).

You do not need to send `X-API-Version` on requests — it is a response-only header.
