npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cloudbase/realtime-js

v1.0.1

Published

Listen to realtime updates to your PostgreSQL database

Downloads

362

Readme

CloudBase Realtime JS SDK

Send ephemeral messages with Broadcast, track and synchronize state with Presence, and listen to database changes with Postgres Change Data Capture (CDC).

This package is @cloudbase/realtime-js. It is standalone and does not depend on the supabase-js monorepo.

Overview

This SDK enables you to use the following Realtime 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

npm install @cloudbase/realtime-js

Creating a Channel

import { RealtimeClient } from '@cloudbase/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.

CloudBase / 微信小程序

通过 @cloudbase/js-sdk 接入时,不要单独 new RealtimeClient()。小程序没有全局 WebSocket / fetch / URL,必须走 host 已注册的 adapter(wsClass / reqClass):

const app = cloudbase.init({ env: 'xxxx-yyy' })
const realtime = app.realtime()
const channel = realtime.channel('room')

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

@cloudbase/app 会在小程序环境自动 useAdapters(adapter-wx_mp)。基础库只要有 wx.connectSocket 即可订阅;httpSend() 走 adapter.reqClass,不依赖浏览器 fetch / URL。

小程序默认开启双连接热切(与浏览器 / Node 相同,270s)。worker 在没有 window.Worker 时不会开启。

真机验证清单

  • [ ] channel.subscribe() 成功(开发者工具 + 真机)
  • [ ] broadcast 收发
  • [ ] 断网后重连
  • [ ] 登录 / 刷新 token 后连接仍可用
  • [ ] (可选)httpSend() 返回成功或明确的 adapter 错误,而不是 fetch is not defined

Broadcast

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

// 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 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:

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.

// 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.

// 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:

// 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):

// 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:

// 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.

import { postgresChangesFilter } from '@cloudbase/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:

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.

// Setup...

client.getChannels()

Cleanup

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

  • Remove a single channel
// Setup...

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

channel.unsubscribe()
client.removeChannel(channel)
  • Remove all channels and close the connection
// Setup...

client.removeAllChannels()
client.disconnect()

Development

This package is standalone. All tooling lives in packages/realtime-js.

Building

cd packages/realtime-js

# Complete build
npm run build

# Individual build targets
npm run build:main    # CommonJS build (dist/main/)
npm run build:module  # ES Modules build (dist/module/)

# Other useful commands
npm run lint
npm run check-exports

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

Validating Package Exports

npm run check-exports

This command uses "Are the types wrong?" to verify that the package exports work correctly in different environments.

Testing

The tests use mocked WebSocket connections and do not need Docker or an external Realtime instance.

cd packages/realtime-js

npm test
npm run test:coverage
npm run test:watch
  • 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

Credits

This package is based on @supabase/realtime-js and draws heavily from phoenix-js.

License

MIT.