---
title: Realtime channels
description: Deliver typed application updates to connected clients without turning events into application state.
pagination: false
sidebar:
  icon: radio
---

A realtime channel sends live application updates to clients that are currently connected. An order-status change, a new announcement, or a dashboard invalidation are good channel events.

> **Start here**
>
> Start with [provider setup](/docs/modules/realtime/providers), then follow the announcement
> example below and [connect a React subscriber](/docs/modules/realtime/react). The [Realtime
> reference](/docs/api/realtime) lists channel declarations and operations.

The channel is not your database. Load the current state from an ordinary route, then use channel events to update or invalidate that state. This distinction makes refresh and reconnect behavior predictable: the route restores the truth, while the channel keeps the page current.

## Pre-setup

Use Redis in local Docker for replay, fanout, and presence. Start from an existing RELKIT app and a running Docker daemon.

1. **Install Redis and Docker support**

    {/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

    From this guide's application/package root, install only its selected requirements:

    ```sh
    bun add @relkit/docker@0.7.12 @relkit/redis@0.8.1
    ```

    Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in `dependencies`; build/type tooling belongs in `devDependencies`. Installing an adapter does not supply its optional SDK peers or enable the feature's configuration.

2. **Configure the default realtime profile**

    > **Merge with your application**
    >
    > The root configuration helper is [`defineApp`](/docs/learn/concepts/application), not
    > `defineConfig`. Merge the capability entries below into your existing `relkit.config.ts`; keep its
    > ID, environment, profiles, and other settings. The empty `defineEnv({})` in a standalone example
    > means this local provider needs no application variables. Keep your existing environment contract.

    ```ts title="relkit.config.ts"
    /** Channels use Docker Redis by default during local development. */
    import { defineApp, defineEnv } from "@relkit/app/config";
    import { docker } from "@relkit/docker";
    import { redisRealtime } from "@relkit/redis";

    export default defineApp({
      env: defineEnv({}),
      realtime: docker(redisRealtime()),
      defaults: { realtime: "default" },
    });
    ```

    Channels without `profile` use `default`. Docker supplies Redis's connection values, so there
    are no additional application environment fields. Agent checkpoints need the separate
    [agent-state configuration](/docs/modules/ai#pre-setup).

3. **Check and start after declaring a channel**

    Check Docker before continuing. After adding the resource and its service registration, run from the app root:

    ```sh title="Terminal — application root"
    docker version
    bun run check
    bun run typecheck
    bun run dev
    ```

    `check` validates the graph; it starts no containers. `dev` starts only Docker recipes used by
    that graph, waits for readiness, and binds their generated connection values. An unused profile
    alone does not start a container. Keep the server running and use `bun run relkit local status`
    in another terminal to inspect the services. [Local Docker](/docs/modules/integrations/local-docker)
    covers detached startup and stopping services while retaining their volumes.

## Realtime channels or application events?

RELKIT has two event-shaped features with different jobs:

| Use                                            | Choose                                       | Why                                                     |
| ---------------------------------------------- | -------------------------------------------- | ------------------------------------------------------- |
| Notify browsers that are viewing a resource    | Realtime channel                             | It supports scoped subscriptions, replay, and presence. |
| Run backend work after something happens       | [Application event](/docs/modules/events)    | It delivers work to server-side consumers.              |
| Return a sequence to the caller of one request | [Typed stream](/docs/modules/http/streaming) | Its lifetime belongs to that request.                   |

A function may publish both an application event and a channel update when both outcomes are needed. Neither mechanism replaces the other.

Use a [job watch](/docs/modules/jobs/clients) for one accepted durable run. Realtime channels remain application
notifications and do not become a substitute for task output, run status, or provider-backed recovery.

## The Realtime API

| API                     | What you use it for                                                                 |
| ----------------------- | ----------------------------------------------------------------------------------- |
| `defineChannel()`       | Declare partition parameters, event payloads, browser access, replay, and presence. |
| `channel.trigger()`     | Append one validated event from backend code.                                       |
| `channel.getPresence()` | Read declared presence without joining the channel or changing its leases.          |
| `useChannel()`          | Observe one typed channel partition from React.                                     |
| `useRealtime()`         | Manage subscriptions directly when a hook per channel is not convenient.            |

1. ## Declare a channel

    > **Before you begin**
    >
    > Before running this declaration in your own lean app, complete [provider installation and
    > binding](/docs/modules/realtime/providers). For a browser consumer, also complete [frontend client
    > setup](/docs/modules/client) in its own package.

    This public announcements channel has one partition because its parameter schema is empty. It retains recent events for brief reconnects.

    ```ts title="examples/commerce/src/services/announcements/channels/announcements.channel.ts"
    /** Declares public posted-announcement events with five-minute replay retention and a thousand-event limit. */
    import { defineChannel } from "@relkit/app/realtime";
    import { z } from "@relkit/app/schema";

    const announcements = defineChannel({
      id: "announcements.feed",
      params: z.object({}),
      events: { posted: z.object({ message: z.string(), postedAt: z.string() }) },
      client: { public: true },
      replay: { retentionMs: 300_000, maxEvents: 1_000 },
    });

    export default announcements;
    ```

    ### Subscribe from the frontend

    After [frontend setup and pulling the commerce contract](/guides/frontend/connect-react), render
    this component under the provider. It subscribes to this declaration's `announcements.feed` channel:

    ```tsx title="src/Announcements.tsx"
    "use client";

    /** Combines the commerce announcement query with channel invalidation and reconnect recovery. */
    import { useChannel, useRoute } from "@relkit/client/react";

    /** Restores authoritative messages on publication, catch-up, and replay gaps.
     * @returns The current messages with query failure and channel status feedback. */
    export function Announcements() {
      const messages = useRoute("GET /announcements", { input: {} });
      const channel = useChannel("announcements.feed", {
        params: {},
        on: {
          posted: () => {
            void messages.refetch();
          },
        },
        onCaughtUp: async () => {
          await messages.refetch();
        },
        onGap: async () => {
          await messages.refetch();
        },
      });

      if (messages.isError) return <p role="alert">{messages.error.message}</p>;
      return (
        <section>
          <p role="status">Channel: {channel.status}</p>
          {messages.isPending && <p>Loading messages…</p>}
          <ul>
            {messages.data?.messages.map((message, index) => (
              <li key={index}>{message}</li>
            ))}
          </ul>
        </section>
      );
    }
    ```

    `params: {}` selects the channel's empty partition schema. `posted` is a declared event, so its
    callback is checked against the event map. The route query restores the authoritative messages
    after updates, catch-up, or replay gaps. Backend `trigger()` publishes; the browser subscribes.

    Reference: [`useChannel`](/docs/api/client#usechannel), [`useRealtime`](/docs/api/client#userealtime).
    Continue with [subscription and recovery](/docs/modules/realtime/react).

    The declaration controls both publication and generated client types:

    | Option     | Meaning                                                                           |
    | ---------- | --------------------------------------------------------------------------------- |
    | `id`       | Stable channel name used by backend and generated clients.                        |
    | `params`   | Schema that identifies a partition, such as one `orderId`.                        |
    | `events`   | Map of event names to their payload schemas.                                      |
    | `client`   | Omit for backend-only use, set `public: true`, or provide an authorization guard. |
    | `replay`   | Optional retention window and maximum event count for reconnects.                 |
    | `presence` | Optional connection count or protected member projection.                         |
    | `profile`  | Provider profile; otherwise the application default is used.                      |

    An empty `events` map is valid only for a presence-only channel. Event binding is never an access-control boundary; browser access always comes from `client`.

    This declaration generates the browser selector `announcements.feed`. The
    [frontend subscriber example](/docs/modules/realtime/react#subscribe-to-a-channel) shows its typed
    `posted` callback, authoritative route query, and reconnect recovery.

2. ## Publish an update

    Call `trigger()` from backend code after the application change succeeds:

    ```ts title="examples/commerce/src/services/announcements/functions/post-announcement.function.ts"
    /** Inserts a validated announcement, removes older demo rows and publishes its posted event before acknowledgement. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";
    import announcements from "../channels/announcements.channel.js";

    const postAnnouncement = defineFunction({
      input: z.object({ message: z.string().min(1) }),
      output: z.object({ accepted: z.literal(true) }),
      handler: async ({ message }, context) => {
        await context.database.announcements.insert({
          data: { message, createdAt: new Date().toISOString() },
        });
        const stale = await context.database.announcements.findMany({
          orderBy: { field: "id", direction: "desc" },
          limit: 1_000,
          offset: 50,
        });
        for (const { id } of stale) {
          await context.database.announcements.delete({ where: { id } });
        }
        await announcements.trigger({}, "posted", { message, postedAt: new Date().toISOString() });
        return { accepted: true as const };
      },
    });

    export default postAnnouncement;
    ```

    RELKIT validates the partition parameters and payload before appending the event. The returned receipt contains the event ID, checkpoint, provider profile and epoch, and whether an idempotent request was a duplicate.

    `accepted: true` means the provider stored the append and its receipt atomically. It does not mean every subscriber received the event. A subscriber can disconnect immediately after the append and recover according to the channel's replay policy.

    For a retryable publish, pass an `idempotencyKey` created by RELKIT. Reusing the same UUIDv7 key with the same channel, partition, event, and payload during the 24-hour receipt window returns the original receipt. Reusing it for different content fails. Without a key, retrying after a lost response may append the event twice; RELKIT never retries it automatically.

    If a database update and publication must succeed together, write an outbox entry in the same database transaction and publish from the outbox. Calling `trigger()` after a database write does not make the two operations atomic.

3. ## Run the backend example offline

    The snippets above belong to the repository's `examples/commerce` app. From the repository
    root, with dependencies installed, run its focused HTTP test:

    ```sh title="Terminal — RELKIT repository root"
    bun test examples/commerce/tests/http.test.ts
    ```

    The test creates an application with an isolated local realtime provider, posts announcements
    through the public HTTP route, and reads them back. Expect two passing tests; no Docker or
    frontend is needed for this check. A passing test proves the function can append its channel
    event, but does not prove browser delivery or reconnect behavior. The
    [React guide](/docs/modules/realtime/react) connects a browser after this backend path works, and
    [Providers](/docs/modules/realtime/providers) explains the local Redis setup for a live server.

## Choose the next step

[Access and revocation](/docs/modules/realtime/access) explains internal, public, and protected
channels.

[React channels](/docs/modules/realtime/react) connects generated channel types to application
state.

[Replay and recovery](/docs/modules/realtime/recovery) defines mount, reconnect, refresh, and
gap behavior.

[Presence](/docs/modules/realtime/presence) adds connection or member awareness.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/realtime` API](/docs/api/realtime)
- [`@relkit/client` API](/docs/api/client)

- [`examples/commerce/src/services/announcements/channels/announcements.channel.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/services/announcements/channels/announcements.channel.ts)
