Realtime
WebSocket channels for live updates, chat, and presence, for apps on hosts that can't hold sockets.
Serverless hosts (Vercel, Netlify, …) can't keep a WebSocket open. mybase can: browsers connect to mybase and subscribe to channels, your backend publishes over HTTP, and mybase fans the messages out. Like Pusher or Ably, on your own machine.
mybase realtime enable shop # prints the socket URL and the secretHow it fits together:
- Your server signs a token for each browser with
MYBASE_REALTIME_SECRET: who the user is and which channels they may subscribe or publish to. No call to mybase. - The browser connects to
https://mybase.example.com/realtime/shopwithconnectRealtimeand subscribes to channels. - Your server publishes with an API key (
realtime:write), or browsers publish to each other where their token allows. - Optionally, a webhook tells your server what browsers did (messages, presence changes).
Tokens (your server)
import { createRealtimeToken } from "@mybase-dev/sdk/realtime";
// e.g. GET /api/realtime-token, behind your own auth
const token = await createRealtimeToken(process.env.MYBASE_REALTIME_SECRET!, {
project: "shop",
userId: user.id, // shown to others in presence channels and on messages
userInfo: { name: user.name }, // ≤ 1 KB, seen by presence members
subscribe: [`user:${user.id}`, "presence:lobby", "orders:*"],
publish: ["presence:lobby"], // default: none (only your server publishes)
ttl: 3600, // seconds, max 86400
});Patterns are exact names, prefix*, or *.
Browser
import { connectRealtime } from "@mybase-dev/sdk/realtime";
const rt = connectRealtime({
apiUrl: "https://mybase.example.com",
project: "shop",
token: () => fetch("/api/realtime-token").then((r) => r.text()),
});
const orders = rt.channel(`user:${userId}`);
orders.on("order.shipped", (data) => toast(`Order ${data.orderId} shipped`));Pass token as a function: it's called on every (re)connect and again before the token expires. The client reconnects with backoff and resubscribes on its own.
Publishing (your server)
import { createRealtimeClient } from "@mybase-dev/sdk/realtime";
const realtime = createRealtimeClient({ apiUrl: "https://mybase.example.com", project: "shop", apiKey: process.env.MYBASE_API_KEY! });
await realtime.publish(`user:${userId}`, "order.shipped", { orderId });
await realtime.publishBatch([{ channel: "orders:42", event: "updated", data: { status: "paid" } }]); // ≤ 100, 64 KBOr plain HTTP: POST https://mybase.example.com/realtime/shop/publish with Authorization: Bearer <key> and { "channel", "event", "data" } (or { "messages": [...] }).
Channels
- Plain (
orders:42): need a token that allows them. public:…: browsers without a token may listen (never publish). Turn off withmybase realtime settings shop --public off.presence:…: track who's in them. Needs a token with auserId; several tabs of one user count once.channel.members()andchannel.onPresence(…)in the browser,realtime.members(channel)on the server.
Names: letters, digits, and _-:.=@, up to 128 characters. Event names: up to 64 of letters, digits, _-:..
Webhooks
Set a URL to hear about browser activity on your server: messages browsers publish, presence joins and leaves, and channels becoming occupied or empty.
mybase realtime settings shop --webhook https://shop.example.com/api/realtime-webhook --events message,presenceimport { verifyRealtimeWebhook } from "@mybase-dev/sdk/realtime";
export async function POST(req: Request) {
const { events } = await verifyRealtimeWebhook(req, process.env.MYBASE_REALTIME_SECRET!);
for (const e of events) if (e.type === "message") await saveChat(e.channel, e.uid, e.data);
return new Response("ok");
}Calls are batched (up to 100 events, about once a second), signed like cron calls, and retried after 10 s, 60 s, and 5 min on errors. Messages are delivered to browsers without waiting for the webhook.
Settings and limits
mybase realtime settings shop [--origins https://shop.example.com] [--max-connections 500] [--max-channels 100] [--max-message-bytes 32768] [--max-rate 20]
- Allowed origins: sites whose pages may connect (empty = any). Browsers enforce
Origin; other clients can fake it, so tokens are the real control. - Limits (defaults): 500 connections per project, 100 channels per connection, 32 KB frames, 20 frames per second per connection. Past the rate, frames are refused; 5 s over closes the socket.
- Secret (
mybase realtime key shop [--rotate]): rotating closes every connection; SDK clients reconnect with fresh tokens.
Security
- Sign tokens on your server with a short expiry and only the channels the user may join.
public:channels are open to anyone who knows the project name. - Set allowed origins, and verify webhooks with
verifyRealtimeWebhook.
More: Security best practices.
Agent notes
- Never send
MYBASE_REALTIME_SECRETor an API key to the browser: tokens are signed on the server. - Give each token the narrowest channel patterns that work (
user:<id>, not*). - State lives in memory on one mybase instance: no message history. If a browser is offline when a message goes out, it misses it; keep the source of truth in the database and use realtime to say "something changed".
- Through a Cloudflare Tunnel, sockets work as-is; the SDK pings every 25 s so idle ones stay open.
- MCP tools:
realtime_stats,list_realtime_channels,realtime_publish.