# Realtime Subscriptions, Broadcast and Presence | Volcano

URL: https://volcano.dev/features/realtime

> Realtime on Volcano: subscribe to Postgres row changes filtered by your row-level policies, broadcast ephemeral messages, and track presence over one WebSocket, with no change-capture pipeline to run.

## Live data without a second backend

Postgres change streams, broadcast and presence over one WebSocket, with the same row-level policies deciding what each subscriber sees.

```console
$ volcano cloud config deploy
realtime  enabled · broadcast · presence · postgres_changes
✓ wss://…/realtime/v1/websocket ready
# then, in the browser
$ channel.onPostgresChanges('INSERT', 'public', 'cards', render)
INSERT public.cards  { id: 'c_18f…', title: 'Ship it' }
# subscribers only receive rows their policies already allow
```

## The usual options are polling or a second system

Either you poll every few seconds and send a thousand queries to find out nothing changed, or you stand up a socket tier, a pub/sub broker and a change-capture pipeline, then keep three deployments and one authorization model in agreement.

Volcano puts change streams, broadcast and presence on the database you already have. Subscribe to a table and Volcano installs what it needs to stream that table's changes. Every message is filtered by the same row-level policies that guard a normal query, so the socket needs no permission model of its own.

## One socket, many channels

*Transport*

Channels share the socket, so a screen watching two tables plus a cursor feed and a presence roster still opens one connection. Reconnection, backoff and re-subscribing belong to the SDK.

- Up to 100 channels per connection on Hobby or 500 on Superagent
- Change streams, broadcast and presence on the same socket
- Reconnects and re-subscribes for you

```console
postgres:public:cards        subscribed
postgres:public:comments     subscribed
broadcast:board-42-cursors   subscribed
broadcast:board-42-typing    subscribed
presence:board-42            subscribed · 7 here
✓ sockets open: 1
```

## Subscribing is the whole setup

*Setup*

Subscribe from the client and Volcano installs the triggers that table needs on the first subscription. There is no replication slot or change-capture pipeline to configure. The table needs row-level security and a primary key, and that is the whole setup.

- Triggers installed on the first subscription
- No replication or change-capture pipeline to run
- Subscribe to one event type or all

Image: Realtime settings with change streams, broadcast and presence enabled.

## The stream obeys your policies

*Authorization*

A change event is delivered to a subscriber only if that subscriber could have selected the row. The rule is the row-level policy you already wrote, so there is no second permission model for the socket.

- Filtered per subscriber, not per channel
- Anon key plus the user's token to connect
- Service keys skip policies, so keep them on a server

```console
# update cards set title = '…' where id = 'c_18f…'
ada   (member of the board)   → UPDATE delivered
cleo  (not a member)          → nothing
same policy as select, applied to the stream
```

## What realtime gives you

- **Postgres change streams**: Subscribe to inserts and updates on any table with row-level security. A server holding the service key also sees deletes.
- **Broadcast**: Send a message to everyone on a channel. Nothing is stored, so it suits cursors and typing indicators.
- **Presence**: Track who is on a channel. The full roster is synced to everyone whenever it changes.
- **Policy-filtered delivery**: Each subscriber receives only the rows their row-level policies would return.
- **One managed WebSocket**: Many channels share one connection, with reconnection handled by the SDK.
- **Turned on in config**: Enable broadcast, presence and change streams in volcano-config.yaml and apply with one command.

## Connect, subscribe, publish

### Subscribe

```typescript
import { VolcanoRealtime } from '@volcano.dev/sdk/realtime';

const realtime = new VolcanoRealtime({
  apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
  anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
  accessToken: volcano.accessToken,
  // With a client and a database, the SDK fetches the changed row for you.
  volcanoClient: volcano,
  databaseName: 'app',
});

await realtime.connect();

// A Postgres channel is named after the table it streams: schema:table.
const cards = realtime.channel('public:cards', { type: 'postgres' });

cards.onPostgresChanges('*', 'public', 'cards', (change) => {
  // change: { type, schema, table, id, record?, timestamp }. The notification
  // carries the primary key; record is the fetched row, when the fetch worked.
  if (change.record) {
    upsertCard(change.record);
  } else {
    refetchCard(change.id);
  }
});

await cards.subscribe();
```

### Broadcast

```typescript
const cursors = realtime.channel('board-42-cursors', { type: 'broadcast' });

cursors.on('cursor', ({ userId, x, y }) => {
  drawCursor(userId, x, y);
});

await cursors.subscribe();

// Nothing is written down. Only clients currently subscribed see this.
// 'type' is what routes the message to the matching .on() listener.
window.addEventListener('pointermove', (event) => {
  cursors.send({ type: 'cursor', userId: me.id, x: event.clientX, y: event.clientY });
});
```

### Presence

```typescript
const room = realtime.channel('board-42', { type: 'presence' });

type Profile = { display_name?: string; avatar_url?: string };

room.onPresenceSync((state) => {
  // Full roster, recomputed after every join and leave. Each entry carries the
  // user's id and the metadata they signed up with.
  renderAvatars(
    Object.values(state).map(({ connInfo }) => {
      const profile = connInfo?.user_metadata as Profile | undefined;
      return { name: profile?.display_name, avatar: profile?.avatar_url };
    }),
  );
});

// Subscribing puts you on the roster, and you drop off it when the socket closes.
await room.subscribe();

// Later
room.unsubscribe();
```

### Enable

```yaml
version: 1

realtime:
  enabled: true
  broadcast_enabled: true
  presence_enabled: true
  postgres_changes_enabled: true

# Apply it, and diff it against the project first if you want to be sure:
#   volcano cloud config deploy --dry-run
#   volcano cloud config deploy
#
# There is no realtime CLI to learn. Streams are policy-filtered, so the
# tables you expose here are still governed by their row-level policies.
```

## What people make live

### Collaborative editing

Cards move, comments appear, and everyone sees who else is in the room. Table changes carry the durable state, broadcast carries cursors, presence carries the roster.

### Dashboards that stay current

A metrics table updates and every open dashboard follows it, without a refresh button or a polling interval you have to tune.

### Long jobs reporting back

A function processing an import broadcasts progress on a channel. The page shows a real percentage instead of a spinner, and nobody polls a status endpoint.

## Works with the rest of Volcano

- [The tables you stream](https://volcano.dev/features/postgres): Change events come from your own Postgres tables, and the read policy decides who receives each one.
- [Who is connected](https://volcano.dev/features/authentication): Subscribers connect with their session token, so presence and filtering use a real user id.
- [Progress from a job](https://volcano.dev/features/edge-functions): A function can broadcast on a channel while it works, so the page updates without polling.
- [Live in the browser](https://volcano.dev/features/nextjs-hosting): The realtime client runs in a Next.js client component alongside the rest of the SDK.

## Frequently asked questions

### Do I have to set up change data capture?

No. The first subscription to a table installs the triggers Volcano needs, and its changes stream from then on. There is no replication to configure. The table needs row-level security and a primary key.

### Can a subscriber see rows they are not allowed to read?

No. Change events are filtered per subscriber using the same row-level policies that apply to a normal query, so a user receives an event only if they could have selected that row.

### What is the difference between broadcast and postgres changes?

Postgres changes follow writes to your tables and are durable because the row is. Broadcast sends a message to whoever is subscribed right now and stores nothing, which suits cursors and typing indicators.

### How many channels can one connection carry?

Up to 100 on Hobby or 500 on Superagent. Channels multiplex over the single socket, so a screen watching two tables plus a cursor feed and a presence roster still holds one connection open. Connection and message limits are on the pricing page.

### What does a change event contain?

The event type, the table and the row's primary key. Give the realtime client your Volcano client and database name, and the SDK fetches the current row for inserts and updates as well. Previous values are not sent, and a signed-in user's subscription does not receive deletes; a server holding the service key does.

### How do I turn realtime on?

Set the realtime block in volcano-config.yaml and run volcano cloud config deploy. There is no realtime CLI and nothing to deploy separately.

## Get started

Ready to make it live? Create a free account at https://volcano.dev/signup, or have your coding agent follow the setup playbook at https://volcano.dev/setup.md.
