File size: 15,172 Bytes
c212805
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
<br />
<p align="center">
  <a href="https://supabase.io">
        <picture>
      <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/supabase-logo-wordmark--dark.svg">
      <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/supabase-logo-wordmark--light.svg">
      <img alt="Supabase Logo" width="300" src="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/logo-preview.jpg">
    </picture>
  </a>

  <h1 align="center">Supabase Realtime JS SDK</h1>

  <h3 align="center">Send ephemeral messages with <b>Broadcast</b>, track and synchronize state with <b>Presence</b>, and listen to database changes with <b>Postgres Change Data Capture (CDC)</b>.</h3>

  <p align="center">
    <a href="https://supabase.com/docs/guides/realtime">Guides</a>
    ·
    <a href="https://supabase.com/docs/reference/javascript">Reference Docs</a>
    ·
    <a href="https://multiplayer.dev">Multiplayer Demo</a>
  </p>
</p>

<div align="center">

[![Build](https://github.com/supabase/supabase-js/workflows/CI/badge.svg)](https://github.com/supabase/supabase-js/actions?query=branch%3Amaster)
[![Package](https://img.shields.io/npm/v/@supabase/realtime-js)](https://www.npmjs.com/package/@supabase/realtime-js)
[![License: MIT](https://img.shields.io/npm/l/@supabase/supabase-js)](#license)
[![pkg.pr.new](https://pkg.pr.new/badge/supabase/realtime-js)](https://pkg.pr.new/~/supabase/realtime-js)

</div>

# Overview

This SDK enables you to use the following Supabase Realtime's features:

- **Broadcast**: send ephemeral messages from client to clients with minimal latency. Use cases include sharing cursor positions between users.
- **Presence**: track and synchronize shared state across clients with the help of CRDTs. Use cases include tracking which users are currently viewing a specific webpage.
- **Postgres Change Data Capture (CDC)**: listen for changes in your PostgreSQL database and send them to clients.

# Usage

## Installing the Package

```bash
npm install @supabase/realtime-js
```

## Creating a Channel

```js
import { RealtimeClient } from '@supabase/realtime-js'

const client = new RealtimeClient(REALTIME_URL, {
  params: {
    apikey: API_KEY,
  },
})

const channel = client.channel('test-channel', {})

channel.subscribe((status, err) => {
  if (status === 'SUBSCRIBED') {
    console.log('Connected!')
  }

  if (status === 'CHANNEL_ERROR') {
    console.log(`There was an error subscribing to channel: ${err.message}`)
  }

  if (status === 'TIMED_OUT') {
    console.log('Realtime server did not respond in time.')
  }

  if (status === 'CLOSED') {
    console.log('Realtime channel was unexpectedly closed.')
  }
})
```

### Notes:

- `REALTIME_URL` is `'ws://localhost:4000/socket'` when developing locally and `'wss://<project_ref>.supabase.co/realtime/v1'` when connecting to your Supabase project.
- `API_KEY` is a JWT whose claims must contain `exp` and `role` (existing database role).
- Channel name can be any `string`.
- Setting `private` to `true` means that the client will use RLS to determine if the user can connect or not to a given channel.

## Broadcast

Your client can send and receive messages based on the `event`.

```js
// Setup...

const channel = client.channel('broadcast-test', { broadcast: { ack: false, self: false } })

channel.on('broadcast', { event: 'some-event' }, (payload) => console.log(payload))

channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    // Send message to other clients listening to 'broadcast-test' channel
    await channel.send({
      type: 'broadcast',
      event: 'some-event',
      payload: { hello: 'world' },
    })
  }
})
```

### Notes:

- Setting `ack` to `true` means that the `channel.send` promise will resolve once server replies with acknowledgment that it received the broadcast message request.
- Setting `self` to `true` means that the client will receive the broadcast message it sent out.

### Broadcast Replay

Broadcast Replay enables **private** channels to access messages that were sent earlier. Only messages published via [Broadcast From the Database](https://supabase.com/docs/guides/realtime/broadcast#trigger-broadcast-messages-from-your-database) are available for replay.

You can configure replay with the following options:

- **`since`** (Required): The epoch timestamp in milliseconds, specifying the earliest point from which messages should be retrieved.
- **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25.

Example:

```typescript
const twelveHours = 12 * 60 * 60 * 1000
const twelveHoursAgo = Date.now() - twelveHours

const config = { private: true, broadcast: { replay: { since: twelveHoursAgo, limit: 10 } } }

supabase
  .channel('main:room', { config })
  .on('broadcast', { event: 'my_event' }, (payload) => {
    if (payload?.meta?.replayed) {
      console.log('This message was sent earlier:', payload)
    } else {
      console.log('This is a new message', payload)
    }
    // ...
  })
  .subscribe()
```

## Presence

Your client can track and sync state that's stored in the channel.

```js
// Setup...

const channel = client.channel('presence-test', {
  config: {
    presence: {
      key: '',
    },
  },
})

channel.on('presence', { event: 'sync' }, () => {
  console.log('Online users: ', channel.presenceState())
})

channel.on('presence', { event: 'join' }, ({ newPresences }) => {
  console.log('New users have joined: ', newPresences)
})

channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
  console.log('Users have left: ', leftPresences)
})

channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    const status = await channel.track({ user_id: 1 })
    console.log(status)
  }
})
```

> `config.presence.enabled` (set automatically if you add an `.on('presence', ...)` listener)
> controls whether _this_ client receives presence state and updates from other clients —
> without it, `presenceState()` stays empty for you. It does not affect whether other clients
> see you: calling `track()` always makes you visible to subscribers that do have presence
> enabled. On RLS-protected channels, receiving presence updates additionally requires the
> `presence.read` policy to authorize this client.

## Postgres CDC

Receive database changes on the client.

```js
// Setup...

const channel = client.channel('db-changes')

channel.on('postgres_changes', { event: '*', schema: 'public' }, (payload) => {
  console.log('All changes in public schema: ', payload)
})

channel.on(
  'postgres_changes',
  { event: 'INSERT', schema: 'public', table: 'messages' },
  (payload) => {
    console.log('All inserts in messages table: ', payload)
  }
)

channel.on(
  'postgres_changes',
  { event: 'UPDATE', schema: 'public', table: 'users', filter: 'username=eq.Realtime' },
  (payload) => {
    console.log('All updates on users table when username is Realtime: ', payload)
  }
)

channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    console.log('Ready to receive database changes!')
  }
})
```

### Filters

The `filter` option accepts **either** a raw string **or** a
`postgresChangesFilter()` builder — both produce the exact same wire format, so
you can mix and match and existing string filters keep working unchanged:

```js
// Raw string — always supported, fully backward compatible
{ event: 'UPDATE', schema: 'public', table: 'users', filter: 'id=eq.1' }

// Builder — type-checked, ergonomic; the SDK serializes it for you
{ event: 'UPDATE', schema: 'public', table: 'users', filter: postgresChangesFilter().eq('id', 1) }
```

A filter is a `column=operator.value` expression evaluated server-side. The
following operators are supported:

| Operator              | String form                  | Builder                                | Meaning                           |
| --------------------- | ---------------------------- | -------------------------------------- | --------------------------------- |
| `eq`                  | `id=eq.1`                    | `.eq('id', 1)`                         | equal                             |
| `neq`                 | `id=neq.1`                   | `.neq('id', 1)`                        | not equal                         |
| `lt` `lte` `gt` `gte` | `age=gte.18`                 | `.gte('age', 18)`                      | comparison                        |
| `in`                  | `status=in.(active,pending)` | `.in('status', ['active', 'pending'])` | in list                           |
| `like` `ilike`        | `title=like.%foo%`           | `.like('title', '%foo%')`              | pattern match (case in/sensitive) |
| `is`                  | `deleted_at=is.null`         | `.is('deleted_at', null)`              | `IS null/true/false/unknown`      |
| `match` `imatch`      | `title=match.^foo`           | `.match('title', '^foo')`              | POSIX regex match (`~` / `~*`)    |
| `isdistinct`          | `value=isdistinct.1`         | `.isDistinct('value', 1)`              | NULL-safe inequality              |

**Negation** — prefix any operator with `not.` (string) or use
`.not(column, operator, value)` (builder):

```js
// String
{ event: '*', schema: 'public', table: 'posts', filter: 'status=not.in.(draft,archived)' }

// Builder
{
  event: '*',
  schema: 'public',
  table: 'posts',
  filter: postgresChangesFilter().not('status', 'in', ['draft', 'archived']),
}
```

**AND composition** — multiple conditions are combined with commas and applied
as an `AND`. With the builder you just chain calls:

```js
// String
{ event: 'UPDATE', schema: 'public', table: 'orders', filter: 'amount=gt.100,status=in.(open,pending)' }

// Builder — equivalent, chained
{
  event: 'UPDATE',
  schema: 'public',
  table: 'orders',
  filter: postgresChangesFilter().gt('amount', 100).in('status', ['open', 'pending']),
}
```

#### Building filters with `postgresChangesFilter()`

The builder (modeled on the `postgrest-js` filter methods) is the recommended,
type-checked way to compose filters — but it is entirely optional; raw strings
remain fully supported.

```js
import { postgresChangesFilter } from '@supabase/realtime-js'

channel.on(
  'postgres_changes',
  {
    event: 'UPDATE',
    schema: 'public',
    table: 'orders',
    // → 'amount=gt.100,status=not.in.(draft,archived)'
    filter: postgresChangesFilter().gt('amount', 100).not('status', 'in', ['draft', 'archived']),
  },
  (payload) => console.log(payload)
)
```

The builder exposes `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `like`,
`ilike`, `match`, `imatch`, `is`, `isDistinct` and `not`. Call `.build()` if you
need the raw string yourself (e.g. to log it or store it).

**Values are sent verbatim** — the server has no quoting/escaping, so spaces and
quotes are preserved as-is. The server separates conditions by commas outside
parentheses, so a literal comma in a scalar value can't be expressed (commas
inside `in.(…)` are fine); the builder throws on such values rather than
silently producing a broken filter.

> **Note for PostgREST users:** Realtime evaluates filters server-side over a
> single table's WAL — there is no resource embedding (`!inner`, embedded
> filters) and no `or()` grouping. Use `%` (not `*`) for `like`/`ilike`
> wildcards, since filters travel in the WebSocket payload rather than a URL.

### Selecting columns

Use `select` to receive only a subset of columns instead of the full row. This
reduces payload size (helpful for large `bytea`/`jsonb` columns). The selected
columns must be selectable by the subscribing role:

```js
channel.on(
  'postgres_changes',
  { event: '*', schema: 'public', table: 'users', select: ['id', 'first_name'] },
  (payload) => {
    // payload.new only contains { id, first_name }
    console.log(payload)
  }
)
```

## Get All Channels

You can see all the channels that your client has instantiatied.

```js
// Setup...

client.getChannels()
```

## Cleanup

It is highly recommended that you clean up your channels after you're done with them.

- Remove a single channel

```js
// Setup...

const channel = client.channel('some-channel-to-remove')

channel.unsubscribe()
client.removeChannel(channel)
```

- Remove all channels and close the connection

```js
// Setup...

client.removeAllChannels()
client.disconnect()
```

## Development

This package is part of the [Supabase JavaScript monorepo](https://github.com/supabase/supabase-js). To work on this package:

### Building

```bash
# Complete build (from monorepo root)
pnpm nx build realtime-js

# Build with watch mode for development
pnpm nx build realtime-js --watch

# Individual build targets
pnpm nx build:main realtime-js    # CommonJS build (dist/main/)
pnpm nx build:module realtime-js  # ES Modules build (dist/module/)

# Other useful commands
pnpm nx clean realtime-js         # Clean build artifacts
pnpm nx lint realtime-js          # Run ESLint
pnpm nx typecheck realtime-js     # TypeScript type checking
```

#### Build Outputs

- **CommonJS (`dist/main/`)** - For Node.js environments
- **ES Modules (`dist/module/`)** - For modern bundlers (Webpack, Vite, Rollup)
- **TypeScript definitions (`dist/module/index.d.ts`)** - Type definitions for TypeScript projects

Note: Unlike some other packages, realtime-js doesn't include a UMD build since it's primarily used in Node.js or bundled applications.

#### Validating Package Exports

```bash
# Check if package exports are correctly configured
pnpm nx check-exports realtime-js
```

This command uses ["Are the types wrong?"](https://github.com/arethetypeswrong/arethetypeswrong.github.io) to verify that the package exports work correctly in different environments. Run this before publishing to ensure your package can be imported correctly by all consumers.

### Testing

**No Docker or Supabase instance required!** The realtime-js tests use mocked WebSocket connections, so they're completely self-contained.

```bash
# Run unit tests (from monorepo root)
pnpm nx test realtime-js

# Run tests with coverage report
pnpm nx test:coverage realtime-js

# Run tests in watch mode during development
pnpm nx test:watch realtime-js
```

#### Test Scripts Explained

- **test** - Runs all unit tests once using Vitest
- **test:coverage** - Runs tests and generates coverage report with terminal output
- **test:watch** - Runs tests in interactive watch mode for development

The tests mock WebSocket connections using `mock-socket`, so you can run them anytime without any external dependencies.

### Contributing

We welcome contributions! Please see our [Contributing Guide](../../../CONTRIBUTING.md) for details on how to get started.

For major changes or if you're unsure about something, please open an issue first to discuss your proposed changes.

## Credits

This repo draws heavily from [phoenix-js](https://github.com/phoenixframework/phoenix/tree/master/assets/js/phoenix).

## License

MIT.