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-sdk0.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#
| Method | What it does |
|---|---|
vibe.subscribe(topic, handler) | Start receiving events on topic. Returns { unsubscribe() }. |
vibe.realtimeState | Current 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 listeningEvery 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 itReconnection 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#
| Method | What 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:
awaitit —publishis 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.okif you want to log it. receivers: 0is normal. It counts servers with listeners attached, not browsers, and0simply 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.readings
❌ post/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
executeFunctioninstead.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#
- SDK Reference — the rest of the
@facilio/vibe-sdksurface - CLI Reference —
facilio vibe functionandfacilio vibe jobs - Getting Started — project setup and deploy