# Bun Redis (/docs/adapters/bun) Bun includes a built-in Redis client (`Bun.redis`) that requires no extra package. ```bash bun add forrealtime zod ``` ```ts import { Realtime } from "forrealtime"; import { createBunRedisAdapter } from "forrealtime/adapters/bun"; const realtime = new Realtime({ schema, redis: createBunRedisAdapter(Bun.redis), }); ``` # Custom Adapter (/docs/adapters/custom) If you need a different Redis client or want to wrap an existing one, implement the `RedisAdapter` interface: ```ts type RedisAdapter = { xadd( channel: string, payload: Record, options?: { maxLen?: number; expireAfterSecs?: number; }, ): Promise; xrange( channel: string, args?: { start?: string; end?: string; count?: number; }, ): Promise }>>; xread(args: { channels: string[]; cursors: string[]; blockMs?: number; count?: number; signal?: AbortSignal; }): Promise< Array<{ channel: string; messages: Array<{ id: string; payload: Record }>; }> >; getLatestCursor(channel: string): Promise; }; ``` Method descriptions [#method-descriptions] | Method | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `xadd` | Append a message to a stream. `maxLen` trims the stream. `expireAfterSecs` sets a TTL on the key. | | `xrange` | Read a range of messages from a stream by ID. Used for history fetching. | | `xread` | Block-read from one or more streams simultaneously. Used for the live SSE subscription. `signal` cancels the read when the client disconnects. | | `getLatestCursor` | Return the ID of the most recent entry in a stream, or `null` if empty. Used to initialise new subscriber cursors. | Example: node-redis [#example-node-redis] ```ts import { createClient } from "redis"; // node-redis import type { RedisAdapter } from "forrealtime"; function createNodeRedisAdapter(client: ReturnType): RedisAdapter { return { async xadd(channel, payload, options) { const id = await client.xAdd( channel, "*", Object.fromEntries( Object.entries(payload).map(([k, v]) => [k, JSON.stringify(v)]) ), options?.maxLen ? { TRIM: { strategy: "MAXLEN", threshold: options.maxLen } } : undefined, ); return id; }, async xrange(channel, args) { const entries = await client.xRange( channel, args?.start ?? "-", args?.end ?? "+", args?.count ? { COUNT: args.count } : undefined, ); return entries.map((e) => ({ id: e.id, payload: Object.fromEntries( Object.entries(e.message).map(([k, v]) => [k, JSON.parse(v as string)]) ), })); }, async xread({ channels, cursors, blockMs, count, signal }) { // implementation omitted for brevity return []; }, async getLatestCursor(channel) { const entries = await client.xRevRange(channel, "+", "-", { COUNT: 1 }); return entries[0]?.id ?? null; }, }; } ``` # Redis Adapters (/docs/adapters) forrealtime uses an adapter pattern so you can bring your own Redis client. Two official adapters are included, and you can write your own. * [ioredis](/docs/adapters/ioredis) — Use the popular ioredis client * [Bun Redis](/docs/adapters/bun) — Use Bun's built-in Redis client * [Custom Adapter](/docs/adapters/custom) — Implement the RedisAdapter interface for any Redis client # ioredis (/docs/adapters/ioredis) ```bash bun add forrealtime ioredis zod ``` ```ts import Redis from "ioredis"; import { Realtime } from "forrealtime"; import { createIORedisAdapter } from "forrealtime/adapters/ioredis"; const realtime = new Realtime({ schema, redis: createIORedisAdapter(new Redis(process.env.REDIS_URL)), }); ``` Pass any `ioredis` client instance to `createIORedisAdapter`. This works with standalone Redis, Sentinel, and Cluster setups. # Bun.serve (/docs/examples/bun-serve) ```ts import z from "zod/v4"; import { Realtime, handle } from "forrealtime"; import { createBunRedisAdapter } from "forrealtime/adapters/bun"; import index from "./index.html"; const realtime = new Realtime({ schema: { notification: { alert: z.string(), }, chat: { message: z.object({ text: z.string(), user: z.string() }), }, }, redis: createBunRedisAdapter(Bun.redis), history: { maxLength: 500 }, }); const realtimeHandler = handle({ realtime }); Bun.serve({ routes: { "/": index, "/api/realtime": { GET: realtimeHandler, }, "/api/emit": { POST: async (req) => { const body = await req.json(); await realtime.emit("chat.message", body); return new Response("ok"); }, }, }, development: { hmr: true }, }); ``` # Hono (/docs/examples/hono) ```bash bun add forrealtime hono zod ``` ```ts import { Hono } from "hono"; import z from "zod/v4"; import { Realtime, handle } from "forrealtime"; import { createBunRedisAdapter } from "forrealtime/adapters/bun"; const app = new Hono(); const realtime = new Realtime({ schema: { notification: { alert: z.string(), }, }, redis: createBunRedisAdapter(Bun.redis), }); const realtimeHandler = handle({ realtime }); app.get("/api/realtime", (c) => realtimeHandler(c.req.raw)); export default app; ``` # Examples (/docs/examples) Full integration examples showing how to use forrealtime with different frameworks and runtimes. * [Hono](/docs/examples/hono) — Lightweight HTTP framework * [TanStack Start](/docs/examples/tanstack-start) — Full-stack React with SSR * [Bun.serve](/docs/examples/bun-serve) — Plain Bun HTTP server # TanStack Start (/docs/examples/tanstack-start) 1\. Server route [#1-server-route] ```ts // src/routes/api.realtime.ts import { createFileRoute } from "@tanstack/react-router"; import { getRequest } from "@tanstack/react-start/server"; import Redis from "ioredis"; import { z } from "zod/v4"; import { Realtime, handle } from "forrealtime"; import { createIORedisAdapter } from "forrealtime/adapters/ioredis"; export const realtime = new Realtime({ schema: { notification: { alert: z.string(), }, }, redis: createIORedisAdapter(new Redis(process.env.REDIS_URL)), }); export const Route = createFileRoute("/api/realtime")({ server: { handlers: { GET: () => { const request = getRequest(); return handle({ realtime })(request); }, }, }, }); ``` 2\. Add the provider at the root [#2-add-the-provider-at-the-root] ```tsx // src/routes/__root.tsx import { HeadContent, Scripts, createRootRouteWithContext, } from "@tanstack/react-router"; import type { QueryClient } from "@tanstack/react-query"; import { RealtimeProvider } from "forrealtime/client"; interface MyRouterContext { queryClient: QueryClient; } export const Route = createRootRouteWithContext()({ shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` 3\. Use the hook in a route [#3-use-the-hook-in-a-route] ```tsx // src/routes/index.tsx import { createFileRoute } from "@tanstack/react-router"; import { useState } from "react"; import { createRealtime } from "forrealtime/client"; import type { realtime } from "./api.realtime"; // type-only import const { useRealtime } = createRealtime(); export const Route = createFileRoute("/")({ component: RouteComponent, }); function RouteComponent() { const [messages, setMessages] = useState([]); const { status } = useRealtime({ channels: ["default"], events: ["notification.alert"], onData(payload) { if (payload.event === "notification.alert") { setMessages((prev) => [...prev, `alert: ${payload.data}`]); } }, }); return (

Status: {status}

{JSON.stringify(messages, null, 2)}
); } ``` 4\. Emit from the server [#4-emit-from-the-server] ```ts await realtime.emit("notification.alert", "TanStack Start is live"); ``` # Getting Started (/docs/getting-started) Installation [#installation] Choose your Redis client and install the matching packages. With ioredis [#with-ioredis] ```bash bun add forrealtime ioredis zod ``` With Bun Redis (built-in) [#with-bun-redis-built-in] ```bash bun add forrealtime zod ``` Server quickstart [#server-quickstart] Define a schema, create a `Realtime` instance, and export the handler. ```ts import Redis from "ioredis"; import z from "zod/v4"; import { Realtime, handle } from "forrealtime"; import { createIORedisAdapter } from "forrealtime/adapters/ioredis"; const schema = { notification: { alert: z.string(), }, chat: { message: z.object({ text: z.string(), user: z.string(), }), }, }; const realtime = new Realtime({ schema, redis: createIORedisAdapter(new Redis(process.env.REDIS_URL)), history: { maxLength: 1000, }, }); // Next.js App Router — export as GET handler export const GET = handle({ realtime }); ``` Emit your first event [#emit-your-first-event] Once the handler is running, emit events from any server-side code: ```ts await realtime.emit("notification.alert", "Welcome!"); await realtime.emit("chat.message", { text: "Hello world", user: "lasse", }); ``` React client quickstart [#react-client-quickstart] Wrap your app in `RealtimeProvider` and create a typed hook: ```tsx // realtime.ts import { createRealtime } from "forrealtime/client"; import type { realtime } from "./server"; // type-only import export const { useRealtime } = createRealtime(); ``` ```tsx // App.tsx import { RealtimeProvider } from "forrealtime/client"; export function App({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```tsx // Notifications.tsx import { useState } from "react"; import { useRealtime } from "./realtime"; export function Notifications() { const [messages, setMessages] = useState([]); const { status } = useRealtime({ channels: ["default"], events: ["notification.alert"], onData(payload) { if (payload.event === "notification.alert") { setMessages((prev) => [...prev, payload.data]); } }, }); return (

Status: {status}

    {messages.map((m, i) => (
  • {m}
  • ))}
); } ``` Svelte client quickstart [#svelte-client-quickstart] Call `provideRealtime()` in a parent component and create the same typed `useRealtime()` helper: ```ts // realtime.ts import { createRealtime } from "forrealtime/client/svelte"; import type { realtime } from "./server"; // type-only import export const { useRealtime } = createRealtime(); ``` ```svelte ``` ```svelte

Status: {$status}

    {#each messages as message}
  • {message}
  • {/each}
``` Next steps [#next-steps] * [Server API](/docs/server) — `Realtime`, `handle`, channels, and emitting events * [React client](/docs/react-client) — `RealtimeProvider`, `createRealtime`, `useRealtime` options * [Svelte client](/docs/svelte-client) — `provideRealtime`, `createRealtime`, reactive `useRealtime` options * [Adapters](/docs/adapters) — ioredis, Bun.redis, and custom adapters * [Middleware](/docs/middleware) — gate or reject connections before the stream opens * [History & reconnects](/docs/history) — replay missed events on reconnect * [Plugins](/docs/plugins) — extend Realtime with additional functionality (e.g. Postgres sync) * [Examples](/docs/examples) — Hono and TanStack Start integration # History & Reconnects (/docs/history) forrealtime stores every emitted event in a Redis Stream. When a client reconnects, the server replays any events the client missed — ensuring no messages are lost during brief disconnects. How it works [#how-it-works] 1. Every `realtime.emit()` call appends an entry to a Redis Stream for the target channel 2. The client tracks the **last acknowledged stream ID** for each channel it subscribes to 3. When the `EventSource` reconnects (after a network drop or server restart), it sends the last known IDs to the server via query parameters 4. The server fetches all stream entries after those IDs and replays them before resuming live events This means clients automatically catch up after a disconnect without any extra code on your part. Configuring history [#configuring-history] Set `history.maxLength` on the `Realtime` constructor to limit how many events are kept per stream: ```ts const realtime = new Realtime({ schema, redis, history: { maxLength: 1000, }, }); ``` When the stream exceeds `maxLength`, the oldest entries are trimmed. This keeps memory usage bounded while still supporting reconnect replay for recent events. Fetching history on the server [#fetching-history-on-the-server] Use `channel.history()` to fetch stored messages from any server-side code — useful for server-rendering the initial state of a chat, notification feed, or activity log: ```ts const room = realtime.channel("room:123"); const recent = await room.history({ limit: 50 }); ``` The returned array contains entries sorted oldest-first: ```ts [ { id: "1700000000000-0", event: "chat.message", data: { text: "Hello", user: "ada" } }, { id: "1700000001000-0", event: "chat.message", data: { text: "World", user: "lasse" } }, ] ``` history options [#history-options] | Option | Type | Description | | ------- | -------- | ----------------------------------------------------------- | | `limit` | `number` | Maximum number of entries to return | | `after` | `string` | Stream ID to start from (exclusive) — useful for pagination | Example: server-rendered history [#example-server-rendered-history] Fetch the last 20 messages and pass them as initial state to a client component: ```tsx // page.tsx (Next.js App Router) import { realtime } from "@/lib/realtime"; import { ChatRoom } from "./ChatRoom"; export default async function Page({ params }: { params: { roomId: string } }) { const room = realtime.channel(`room:${params.roomId}`); const history = await room.history({ limit: 20 }); return ; } ``` ```tsx // ChatRoom.tsx "use client"; import { useState } from "react"; import { useRealtime } from "@/lib/realtime"; type Message = { text: string; user: string }; export function ChatRoom({ initialMessages, roomId, }: { initialMessages: { event: string; data: unknown }[]; roomId: string; }) { const [messages, setMessages] = useState( initialMessages .filter((m) => m.event === "chat.message") .map((m) => m.data as Message), ); useRealtime({ channels: [roomId], events: ["chat.message"], onData(payload) { if (payload.event === "chat.message") { setMessages((prev) => [...prev, payload.data]); } }, }); return (
    {messages.map((m, i) => (
  • {m.user}: {m.text}
  • ))}
); } ``` # Introduction (/docs) **forrealtime** gives you typed, framework-agnostic server-sent events backed by Redis Streams. The API stays close to `@upstash/realtime`, but swaps the Redis layer for adapters so you can use ioredis, Bun.redis, or your own client. Why forrealtime? [#why-forrealtime] * **Framework-agnostic SSE handler** — accepts a standard `Request`, works in Next.js, Hono, TanStack Start, and any runtime that speaks `Request`/`Response` * **Typed events from a nested Zod schema** — define your event shape once on the server, infer types on the client with no duplication * **Redis Streams for history replay** — clients reconnect and replay missed events automatically using the last acknowledged stream ID * **Adapter-based Redis integration** — no hard Upstash dependency; bring ioredis, Bun.redis, or write your own adapter * **Small React and Svelte client APIs** — `RealtimeProvider`, `provideRealtime`, `createRealtime`, `useRealtime` How it works [#how-it-works] 1. You define a nested Zod schema that describes your event types 2. `Realtime` owns the schema, Redis adapter, and channel API 3. `handle({ realtime })` turns that into an SSE endpoint that accepts a standard `Request` 4. `RealtimeProvider` (React) or `provideRealtime()` (Svelte) opens one shared `EventSource` for the active channels in your app 5. `createRealtime()` gives you a fully typed `useRealtime()` hook Packages [#packages] | Import path | Contents | | ------------------------------ | --------------------------------------------------------------------------------- | | `forrealtime` | `Realtime`, `handle`, `MiddlewareContext`, `HandleOptions`, `InferRealtimeEvents` | | `forrealtime/client` | `RealtimeProvider`, `createRealtime`, `useRealtime` | | `forrealtime/client/svelte` | `provideRealtime`, `getRealtimeContext`, `createRealtime`, `useRealtime` | | `forrealtime/adapters/ioredis` | `createIORedisAdapter` | | `forrealtime/adapters/bun` | `createBunRedisAdapter` | # Middleware (/docs/middleware) Middleware lets you inspect the incoming request and the requested channels before any SSE stream is opened. Return a `Response` to reject the connection, or return nothing (or `undefined`) to allow it through. Basic usage [#basic-usage] ```ts import { handle } from "forrealtime"; import type { MiddlewareContext } from "forrealtime"; const auth = (ctx: MiddlewareContext) => { const token = ctx.request.headers.get("authorization"); if (token !== "Bearer secret") { return new Response("Unauthorized", { status: 401 }); } }; export const GET = handle({ realtime, middleware: auth }); ``` MiddlewareContext [#middlewarecontext] The middleware function receives a single `MiddlewareContext` argument: ```ts import type { MiddlewareContext } from "forrealtime"; ``` | Property | Type | Description | | ---------- | ---------- | ---------------------------------------------------------- | | `request` | `Request` | The incoming HTTP request (headers, URL, cookies, etc.) | | `channels` | `string[]` | The channel names the client is requesting to subscribe to | Checking channels [#checking-channels] You can inspect the requested channels to apply per-channel authorization: ```ts const auth = (ctx: MiddlewareContext) => { const authorized = ctx.request.headers.get("authorization") === "Bearer secret"; if (!authorized) { return new Response("Unauthorized", { status: 401 }); } if (ctx.channels.includes("admin")) { return new Response("Forbidden", { status: 403 }); } }; ``` Async middleware [#async-middleware] Middleware can be async — useful for database lookups or token verification: ```ts const auth = async (ctx: MiddlewareContext) => { const token = ctx.request.headers.get("authorization")?.replace("Bearer ", ""); if (!token) { return new Response("Unauthorized", { status: 401 }); } const user = await verifyToken(token); if (!user) { return new Response("Unauthorized", { status: 401 }); } // Private channels require admin role const hasPrivateChannel = ctx.channels.some((c) => c.startsWith("private:")); if (hasPrivateChannel && user.role !== "admin") { return new Response("Forbidden", { status: 403 }); } }; export const GET = handle({ realtime, middleware: auth }); ``` Cookie-based auth [#cookie-based-auth] ```ts import { handle } from "forrealtime"; import type { MiddlewareContext } from "forrealtime"; const cookieAuth = (ctx: MiddlewareContext) => { const cookie = ctx.request.headers.get("cookie") ?? ""; const session = parseCookies(cookie).session; if (!session || !isValidSession(session)) { return new Response("Unauthorized", { status: 401 }); } }; export const GET = handle({ realtime, middleware: cookieAuth }); ``` # Plugins (/docs/plugins) Plugins let you extend a `Realtime` instance with new methods — similar to the plugin model in [better-auth](https://www.better-auth.com). Pass them at instantiation and TypeScript automatically infers the added API. ```ts import { Realtime } from "forrealtime"; import { postgresSync } from "forrealtime/plugins/postgres-sync"; const realtime = new Realtime({ schema, redis, plugins: [ postgresSync({ sql: Bun.sql, tables: ["users", "posts"] }), ], }); // Methods added by the plugin are fully typed: await realtime.pg.start(); ``` How it works [#how-it-works] Each plugin receives a `RealtimePluginContext` at startup and returns an `api` object. That object gets merged onto the `Realtime` instance at construction time via `Object.assign`, and TypeScript infers the added methods from the plugin's return type. Writing your own plugin [#writing-your-own-plugin] ```ts import type { RealtimePlugin } from "forrealtime"; function myPlugin(): RealtimePlugin<{ greet(): string }> { return { name: "my-plugin", init(context) { return { api: { greet() { context.logger.log("hello from my plugin"); return "hello!"; }, }, }; }, }; } ``` `RealtimePluginContext` [#realtimeplugincontext] | Property | Type | Description | | -------- | ----------------------------------------- | --------------------------------------------------------------- | | `emit` | `(channel, event, data) => Promise` | Emit an event on any channel | | `logger` | `{ log, warn, error }` | Forwards to the `Realtime` instance logger (respects `verbose`) | Available plugins [#available-plugins] * [postgresSync](/docs/plugins/postgres-sync) — Stream PostgreSQL table changes to realtime clients # postgresSync (/docs/plugins/postgres-sync) `postgresSync` watches one or more PostgreSQL tables and emits a realtime event whenever a row is inserted, updated, or deleted. Clients subscribe to those events just like any other realtime event. Installation [#installation] No extra packages needed — it uses `Bun.sql` which is built into Bun. ```ts import { postgresSync } from "forrealtime/plugins/postgres-sync"; ``` Basic usage [#basic-usage] ```ts import { Realtime } from "forrealtime"; import { createBunAdapter } from "forrealtime/adapters/bun"; import { postgresSync } from "forrealtime/plugins/postgres-sync"; const realtime = new Realtime({ redis: createBunAdapter(Bun.redis), plugins: [ postgresSync({ sql: Bun.sql, tables: ["users", "posts"], }), ], }); // Start listening after the server is ready await realtime.pg.start(); ``` Call `realtime.pg.stop()` to unsubscribe and tear down the LISTEN connections. How it works [#how-it-works] 1. On `start()`, the plugin installs an `AFTER INSERT OR UPDATE OR DELETE` trigger on each table using `CREATE OR REPLACE` (requires PostgreSQL 14+). 2. The trigger calls `pg_notify` with a JSON payload containing the table name, operation, and the affected row. 3. The plugin subscribes via `Bun.sql.listen()` and — for each notification — emits a realtime event. Events are emitted on a channel named after the table, with the event name `{table}.insert`, `{table}.update`, or `{table}.delete`: | Table | Operation | Channel | Event | | ------- | --------- | ------- | -------------- | | `users` | INSERT | `users` | `users.insert` | | `users` | UPDATE | `users` | `users.update` | | `users` | DELETE | `users` | `users.delete` | Subscribing on the client [#subscribing-on-the-client] ```tsx const { status } = useRealtime({ channels: ["users"], events: ["users.insert", "users.update", "users.delete"], onData(payload) { console.log(payload.event, payload.data); // e.g. "users.insert", { id: 1, name: "..." } }, }); ``` Options [#options] | Option | Type | Default | Description | | ----------------- | ------------------------------ | ---------- | --------------------------------------- | | `sql` | `typeof Bun.sql` | — | Bun SQL connection | | `tables` | `Array` | — | Tables to watch | | `pgSchema` | `string` | `"public"` | PostgreSQL schema | | `realtimeChannel` | `string` | table name | Default realtime channel for all tables | Per-table config [#per-table-config] Each entry in `tables` can be a plain string or a `TableConfig` object for per-table control: ```ts postgresSync({ sql: Bun.sql, tables: [ "posts", // plain string — no filter { name: "users", // Only emit events for active users filter: (row, op) => row.active === true, }, { name: "messages", // Skip deletes filter: (row, op) => op !== "DELETE", // Emit on a different realtime channel realtimeChannel: "feed", }, ], }); ``` `TableConfig` [#tableconfig] | Property | Type | Description | | ----------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Table name | | `filter` | `(row, op) => boolean` | Drop the event when this returns `false`. `row` is `NEW` for INSERT/UPDATE, `OLD` for DELETE. `op` is `"INSERT"`, `"UPDATE"`, or `"DELETE"`. | | `realtimeChannel` | `string` | Override the realtime channel for this table | Filter examples [#filter-examples] ```ts // Only sync rows where a column matches filter: (row) => row.status === "published" // Skip soft-deletes (emit the update, not a delete event) filter: (row, op) => op !== "DELETE" // Only emit inserts filter: (row, op) => op === "INSERT" // Combine conditions filter: (row, op) => op !== "DELETE" && row.tenant_id === "acme" ``` `pg` API [#pg-api] | Method | Description | | ------------ | ----------------------------------------------------------------------------------- | | `pg.start()` | Install triggers and start listening. Safe to call once — warns if already started. | | `pg.stop()` | Unlisten from all tables. | # React Client (/docs/react-client) The React client has two steps: 1. Wrap your app (or a subtree) in `RealtimeProvider` 2. Create a typed `useRealtime()` hook with `createRealtime()` ```ts import { RealtimeProvider, createRealtime } from "forrealtime/client"; ``` * [RealtimeProvider](/docs/react-client/provider) — Opens a shared EventSource connection for your component tree * [useRealtime](/docs/react-client/use-realtime) — Subscribe to channels and receive typed events * [Type Inference](/docs/react-client/type-inference) — Infer event types directly from your server schema # RealtimeProvider (/docs/react-client/provider) `RealtimeProvider` opens one shared `EventSource` connection for all channels used anywhere in its subtree. Place it high in your component tree — typically at the root or layout level. ```tsx import { RealtimeProvider } from "forrealtime/client"; export function App({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Props [#props] | Prop | Type | Description | | --------- | -------- | ------------------------ | | `api.url` | `string` | URL of your SSE endpoint | # Type Inference (/docs/react-client/type-inference) Pass `typeof realtime` to `createRealtime` and the hook's types are inferred directly from your Zod schema — no need to duplicate type definitions on the client. Export `realtime` from your server file: ```ts // server.ts export const realtime = new Realtime({ schema, redis }); export const GET = handle({ realtime }); ``` Then import it as a **type** on the client — TypeScript resolves the type without pulling in any server code: ```ts // realtime.ts (client) import { createRealtime } from "forrealtime/client"; import type { realtime } from "./server"; // type-only — no server code in bundle export const { useRealtime } = createRealtime(); ``` You can also pass a plain events type directly if you prefer not to import from the server: ```ts type Events = { notification: { alert: string; }; chat: { message: { text: string; user: string }; }; }; export const { useRealtime } = createRealtime(); ``` InferRealtimeEvents [#inferrealtimeevents] Use `InferRealtimeEvents` when you need the inferred event payload types in other parts of your app — for example to type a function that accepts an event payload: ```ts import type { InferRealtimeEvents } from "forrealtime"; import type { realtime } from "./server"; type Events = InferRealtimeEvents; // { notification: { alert: string }; chat: { message: { text: string; user: string } } } function handleEvent(event: Events["notification"]["alert"]) { console.log(event); // string } ``` # useRealtime (/docs/react-client/use-realtime) `useRealtime` subscribes to one or more channels and fires a callback for each matching event. ```tsx import { useState } from "react"; import { useRealtime } from "./realtime"; export function Notifications() { const [messages, setMessages] = useState([]); const { status } = useRealtime({ channels: ["default"], events: ["notification.alert", "chat.message"], onData(payload) { if (payload.event === "notification.alert") { setMessages((prev) => [...prev, `alert: ${payload.data}`]); } if (payload.event === "chat.message") { setMessages((prev) => [ ...prev, `${payload.data.user}: ${payload.data.text}`, ]); } }, }); return (

Status: {status}

{JSON.stringify(messages, null, 2)}
); } ``` Options [#options] | Option | Type | Default | Description | | ---------- | ------------------- | ------------- | -------------------------------------------------------- | | `channels` | `string[]` | `["default"]` | Channel names to subscribe to | | `events` | `string[]` | all events | Event names to filter on (e.g. `["notification.alert"]`) | | `onData` | `(payload) => void` | — | Callback for each typed message | | `enabled` | `boolean` | `true` | Set to `false` to pause the subscription | Returned state [#returned-state] | Property | Type | Values | | -------- | -------- | ---------------------------------------------------------- | | `status` | `string` | `"connecting"`, `"connected"`, `"disconnected"`, `"error"` | # Channels (/docs/server/channels) Channels let you scope events to a specific group of subscribers — a chat room, a user session, a tenant, etc. realtime.channel [#realtimechannel] ```ts const room = realtime.channel("room:123"); ``` channel.emit [#channelemit] ```ts await room.emit("chat.message", { text: "Room message", user: "ada", }); ``` channel.history [#channelhistory] Fetch stored messages from the channel's Redis Stream: ```ts const recent = await room.history({ limit: 50 }); ``` Returns an array of `{ id: string; event: string; data: unknown }` entries sorted oldest-first. # Emitting Events (/docs/server/emit) realtime.emit [#realtimeemit] Emit an event to the default channel: ```ts await realtime.emit("notification.alert", "Hello"); await realtime.emit("chat.message", { text: "Hello world", user: "lasse", }); ``` The event name is `"namespace.eventName"` and the payload is validated against your Zod schema. TypeScript will infer the correct payload type from the event name. # handle (/docs/server/handle) `handle` converts a `Realtime` instance into an SSE endpoint that accepts a standard `Request` and returns a `Response`. ```ts import { handle } from "forrealtime"; const realtimeHandler = handle({ realtime }); // Next.js App Router export const GET = realtimeHandler; // Hono app.get("/api/realtime", (c) => realtimeHandler(c.req.raw)); // Bun.serve Bun.serve({ routes: { "/api/realtime": { GET: realtimeHandler }, }, }); ``` HandleOptions [#handleoptions] ```ts import type { HandleOptions } from "forrealtime"; ``` | Option | Type | Description | | ------------ | --------------------------------------------------------------------------- | ---------------------------- | | `realtime` | `Realtime` | The Realtime instance | | `middleware` | `(ctx: MiddlewareContext) => Response \| void \| Promise` | Optional middleware function | # Server API (/docs/server) The server API is the core of forrealtime. It provides the `Realtime` class, the `handle` function to create SSE endpoints, channels for scoping events, and utility types for type inference. * [Realtime](/docs/server/realtime) — The core class that owns your schema, Redis adapter, and history settings * [handle](/docs/server/handle) — Convert a Realtime instance into an SSE endpoint * [Emitting Events](/docs/server/emit) — Send events to the default channel or a specific channel * [Channels](/docs/server/channels) — Scope events to specific groups of subscribers * [InferRealtimeEvents](/docs/server/infer-events) — Extract inferred event payload types # InferRealtimeEvents (/docs/server/infer-events) Use this utility type to extract the inferred event payload types from a `Realtime` instance: ```ts import type { InferRealtimeEvents } from "forrealtime"; import type { realtime } from "./server"; type Events = InferRealtimeEvents; // { // notification: { alert: string }; // chat: { message: { text: string; user: string } }; // } ``` This is useful when you need to type a function parameter on the client without importing `createRealtime`. # Realtime (/docs/server/realtime) The `Realtime` class is the core of forrealtime. It owns your schema, Redis adapter, and history settings. ```ts import { Realtime } from "forrealtime"; const realtime = new Realtime({ schema, redis, history: { maxLength: 1000, }, }); ``` Constructor options [#constructor-options] | Option | Type | Description | | ------------------- | ----------------------------------------- | ------------------------------------------------------ | | `schema` | `Record>` | Nested Zod schema defining event types | | `redis` | `RedisAdapter` | Redis adapter instance | | `history.maxLength` | `number` | Maximum number of events to keep per stream | | `plugins` | `RealtimePlugin[]` | Plugins to extend the instance with additional methods | Schema shape [#schema-shape] The schema is a two-level nested object. The top level is the namespace, the second level is the event name, and the value is a Zod type: ```ts import z from "zod/v4"; const schema = { notification: { alert: z.string(), badge: z.number(), }, chat: { message: z.object({ text: z.string(), user: z.string(), }), typing: z.object({ user: z.string(), }), }, }; ``` # Svelte Client (/docs/svelte-client) The Svelte client has two steps: 1. Call `provideRealtime()` in a parent component 2. Create a typed `useRealtime()` helper with `createRealtime()` ```ts import { provideRealtime, createRealtime } from "forrealtime/client/svelte"; ``` * [provideRealtime](/docs/svelte-client/provide-realtime) — Create a shared realtime client for a component subtree * [useRealtime](/docs/svelte-client/use-realtime) — Subscribe to channels and receive typed events * [Type Inference](/docs/svelte-client/type-inference) — Infer event types directly from your server schema # provideRealtime (/docs/svelte-client/provide-realtime) `provideRealtime` creates a `RealtimeClient`, stores it in Svelte context, and destroys it automatically when the parent component unmounts. Call it once in a parent component, typically your root layout or app shell. ```svelte ``` Options [#options] | Option | Type | Description | | --------- | -------- | ------------------------ | | `api.url` | `string` | URL of your SSE endpoint | Return value [#return-value] `provideRealtime()` returns the created `RealtimeClient` if you need to keep a local reference in the provider component. getRealtimeContext [#getrealtimecontext] Use `getRealtimeContext()` in descendant components when you need direct access to the shared client instance instead of the higher-level `useRealtime()` helper. ```ts import { getRealtimeContext } from "forrealtime/client/svelte"; const client = getRealtimeContext(); ``` It throws if no parent component has called `provideRealtime()` first. # Type Inference (/docs/svelte-client/type-inference) Pass `typeof realtime` to `createRealtime` and the hook's types are inferred directly from your Zod schema, so you do not need to duplicate event definitions on the client. Export `realtime` from your server file: ```ts // server.ts export const realtime = new Realtime({ schema, redis }); export const GET = handle({ realtime }); ``` Then import it as a **type** on the client. TypeScript resolves the type without pulling any server code into your Svelte bundle: ```ts // realtime.ts import { createRealtime } from "forrealtime/client/svelte"; import type { realtime } from "./server"; export const { useRealtime } = createRealtime(); ``` You can also pass a plain events type directly if you prefer not to import from the server: ```ts type Events = { notification: { alert: string; }; chat: { message: { text: string; user: string }; }; }; export const { useRealtime } = createRealtime(); ``` InferRealtimeEvents [#inferrealtimeevents] Use `InferRealtimeEvents` when you need the inferred event payload types elsewhere in your app. ```ts import type { InferRealtimeEvents } from "forrealtime"; import type { realtime } from "./server"; type Events = InferRealtimeEvents; function handleEvent(event: Events["notification"]["alert"]) { console.log(event); } ``` # useRealtime (/docs/svelte-client/use-realtime) `useRealtime` subscribes to one or more channels, fires a callback for each matching event, and returns a readable `status` store. ```svelte

Status: {$status}

{JSON.stringify(messages, null, 2)}
``` Options [#options] | Option | Type | Default | Description | | ---------- | ------------------- | ------------- | ------------------------------------------------------------- | | `channels` | `string[]` | `["default"]` | Channel names to subscribe to | | `events` | `string[]` | all events | Event names to filter on (for example `"notification.alert"`) | | `onData` | `(payload) => void` | — | Callback for each typed message | | `enabled` | `boolean` | `true` | Set to `false` to pause the subscription | Reactive options [#reactive-options] If `channels`, `events`, or `enabled` are reactive, pass a Svelte store of options to `useRealtime(...)`. The hook unsubscribes from the previous selection and resubscribes whenever that store emits a new value. ```svelte

Status: {$status}

``` Returned state [#returned-state] | Property | Type | Values | | -------- | ------------------ | ---------------------------------------------------------- | | `status` | `Readable` | `"connecting"`, `"connected"`, `"disconnected"`, `"error"` |