Skip to content

Channels and presence

Channels carry messages between connected clients that never touch your database: typing indicators, cursor positions, game moves. Presence shows which connections are online and what state each one has published.

Both use the same connection as row-change subscriptions, so they need the same client setup: a publishable API key, and the user’s access token when you have one.

Get a channel with vaif.channel(name), listen for events, subscribe, then send:

import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({
baseUrl: "https://api.vaif.studio",
projectId: process.env.VAIF_PROJECT_ID,
apiKey: process.env.VAIF_PUBLISHABLE_KEY,
});
type Typing = { userId: string; typing: boolean };
const room = vaif.channel("room:42");
room.on<Typing>("typing", (payload, meta) => {
console.log(`${payload.userId} typing: ${payload.typing}`, meta?.senderId);
});
await room.subscribe();
await room.broadcast({ event: "typing", payload: { userId: "user-1", typing: true } });
// Leave the channel and forget it:
await vaif.removeChannel(room);

Rules for channel names and messages:

  • A channel name is letters, digits and _ - : . @ /, up to 200 characters. Names that start with table: are reserved for row-change subscriptions.
  • You must be subscribed to a channel before you broadcast on it.
  • The server does not send a broadcast back to the connection that sent it. Pass includeSelf: true to broadcast() to also deliver it to your own listeners.
  • Listen for every event on a channel with room.on("*", handler); meta.event tells you which one arrived.
  • Broadcasts are not stored. A client that is offline when a message is sent never receives it.

Create a presence channel, track your own state, and listen for joins and leaves:

type Cursor = { name: string; x: number; y: number };
const lobby = vaif.channel<Cursor>("lobby", { type: "presence" });
lobby.presence.onSync((state) => {
console.log("online connections:", Object.keys(state).length);
});
lobby.presence.onJoin((key, current, joined) => {
for (const p of joined) console.log(`${p.state.name} joined`);
});
lobby.presence.onLeave((key, current, left) => {
for (const p of left) console.log(`${p.state.name} left`);
});
await lobby.subscribe();
await lobby.presence.track({ state: { name: "Alice", x: 0, y: 0 } });
// Change part of your state later:
await lobby.presence.update({ x: 120, y: 80 });
// Go offline without closing the channel:
await lobby.presence.untrack();

How presence works:

  • Presence is per connection, across the project. Each connection has one presence state, and every presence channel in the project sees the same set of tracked connections. To show who is in one room, include the room in the state you track and filter on it.
  • Entries are keyed by connection id. A user with two tabs open appears twice, once per connection; put a user id in the state if you need to count people.
  • When a connection closes, its presence is removed and a leave is sent to everyone else.
  • lobby.presence.state() returns the current state at any time.
You want to Use
Show a row as soon as it is written Row changes
Send something that should not be stored (typing, cursors) Broadcast
Show who is online Presence

@vaifstudio/react wraps all three: useRealtime, useChannel, useBroadcast and usePresence. See Realtime in React.