Skip to main content

Realtime

Vibe apps can receive live updates over a WebSocket instead of polling.

  • In the browser, you subscribe to a topic.
  • In a server function, you publish to that topic.

Every browser subscribed to the topic gets the event — including other users, on other machines.

server function: events.publish('posts', {...})   →   browser: vibe.subscribe('posts', cb)

Requirements#

  • @facilio/vibe-sdk 0.3.0+ in the browser.
  • A server function to publish from — rebuild it (facilio vibe function build <name>) after adding the publish call.

Nothing to configure in vibe.json, and no CLI command to enable. Subscribing connects.


Client side — @facilio/vibe-sdk#

Methods#

MethodWhat it does
vibe.subscribe(topic, handler)Start receiving events on topic. Returns { unsubscribe() }.
vibe.realtimeStateCurrent state: 'idle', 'connecting', 'open', 'reconnecting', 'closed'.
vibe.onRealtimeState(listener)Get notified when the state changes. Returns a function that removes the listener.
vibe.closeRealtime()Close the connection and drop every subscription.

That's the whole client surface. The browser can subscribe but not publish — to send something to the server, use executeFunction.

Subscribe#

import { createVibe } from '@facilio/vibe-sdk';
const vibe = createVibe();
const sub = vibe.subscribe('posts', (evt) => {  console.log(evt.payload);          // whatever the function published});
sub.unsubscribe();                    // stop listening

Every callback receives:

{  topic: 'posts',                     // the topic you subscribed with  eventId: 'a91f…',                   // unique id for this event  ts: 1769000000000,                  // publish time, epoch ms  payload: {}                      // exactly what the function published}

payload is yours to shape. A type field is a handy convention when one topic carries several kinds of change:

const sub = vibe.subscribe<{ type: string; post: Post }>('posts', (evt) => {  if (evt.payload.type === 'post.created') prependCard(evt.payload.post);  if (evt.payload.type === 'post.deleted') removeCard(evt.payload.post.id);});

In React#

Subscribe in an effect and always return the unsubscribe, so re-renders don't stack duplicate handlers:

useEffect(() => {  const sub = vibe.subscribe<FeedEvent>('posts', (evt) => {    setPosts((prev) => applyEvent(prev, evt.payload));  });  return () => sub.unsubscribe();}, []);

Show a live indicator#

const stop = vibe.onRealtimeState((state) => setLive(state === 'open'));// stop() when you no longer need it

Reconnection is automatic — if the network drops, the SDK reconnects and re-subscribes to all your topics on its own.


Server side — @facilio/studio-functions#

Methods#

MethodWhat it does
new VibeEvents()Create the publisher. Takes no arguments.
await events.publish(topic, payload)Send payload to everyone subscribed to topic. Returns { ok, topic, receivers, error? }.

Publish#

import StudioFunctions, { VibeEvents } from '@facilio/studio-functions';
const server = new StudioFunctions();const events = new VibeEvents();
server.addHandler({  name: 'createPost',  execute: async (args) => {    const post = await insertPost(args);                            // do the work first
    await events.publish('posts', { type: 'post.created', post });  // then notify
    return { ok: true, post };                                      // response to the caller  },});

Then rebuild the function:

facilio vibe function build <name>

Four things to keep in mind:

  • await itpublish is asynchronous.
  • Publish after your work succeeded, never before.
  • It never throws. If delivery fails you get { ok: false, error } and your handler carries on — a lost notification should not fail a completed write. Check .ok if you want to log it.
  • receivers: 0 is normal. It counts servers with listeners attached, not browsers, and 0 simply means nobody has the app open.

You can publish from a function called by the browser, by a scheduled job, or by facilio vibe function run.


Topic names#

A topic is a short name, up to 64 characters, using letters, numbers, ., _ and -:

posts · post.42 · job-progress · asset.183.readingspost/42 · my topic · posts*

Use dots for hierarchy. Pick names that match what a screen needs to redraw: one shared topic (posts) for a feed everyone watches, or a per-record topic (post.42) when only the people on that record care.


Good to know#

  • Preview and production are separate. The same topic name in a preview app and a live app are two different topics, so preview activity never reaches production users.

  • Topics are app-wide. Everyone subscribed to a topic receives everything published on it, so don't put one user's private data on a shared topic — fetch that with executeFunction instead.

  • Events are not stored. If a browser is closed or offline it misses whatever was published in the meantime. Reload the data when the connection opens and let events keep it fresh after that:

    vibe.onRealtimeState((state) => { if (state === 'open') loadFeed(); });
  • Keep payloads small (under 32 KB) and subscribe to at most 20 topics per tab. Send ids and let the app fetch details.

  • Publishing needs a deployed app. The full publish → browser loop can't be exercised against a local dev server; test it on a preview or live app.


Related#