Channels & topics
Channels are the top-level scope for realtime messages. Each channel has one or more topics: typed message streams with schemas that provide end-to-end type safety from publishing to subscribing.
import { realtime } from "inngest";
import { z } from "zod";
const alerts = realtime.channel({
name: "system:alerts",
topics: {
alert: { schema: z.object({ message: z.string(), severity: z.enum(["info", "warn", "error"]) }) },
},
});
realtime.channel(options)
Creates a channel definition. The returned value is either a channel instance (static name) or a factory function (parameterized name).
- Name
name- Type
- string | function
- Required
- required
- Description
The channel name. Use a static string for a fixed channel, or a function that returns a string for parameterized channels. The function receives a single object argument with your parameters.
- Name
topics- Type
- Record<string, { schema }>
- Required
- required
- Description
An object mapping topic names to their configuration. Each topic must have a
schemaproperty: any Standard Schema (Zod, Valibot, ArkType) orstaticSchema<T>()for type-only schemas.
import { realtime, staticSchema } from "inngest";
import { z } from "zod";
// Static channel
const alerts = realtime.channel({
name: "system:alerts",
topics: {
alert: { schema: z.object({ message: z.string() }) },
},
});
// Parameterized channel
const chat = realtime.channel({
name: ({ threadId }: { threadId: string }) => `chat:${threadId}`,
topics: {
message: { schema: z.object({ text: z.string() }) },
typing: { schema: staticSchema<{ userId: string }>() },
},
});
Static channels
When name is a string, realtime.channel() returns a channel instance directly. You can use it without calling it as a function.
const alerts = realtime.channel({
name: "system:alerts",
topics: {
alert: { schema: z.object({ message: z.string() }) },
},
});
// Use directly, no instantiation needed
alerts.name; // "system:alerts"
alerts.alert; // TopicRef for the "alert" topic
alerts.alert.channel // "system:alerts"
alerts.alert.topic // "alert"
Parameterized channels
When name is a function, realtime.channel() returns a factory. Call it with your parameters to get a channel instance.
const chat = realtime.channel({
name: ({ threadId }: { threadId: string }) => `chat:${threadId}`,
topics: {
message: { schema: z.object({ text: z.string(), sender: z.string() }) },
status: { schema: z.object({ typing: z.boolean() }) },
},
});
// Instantiate with parameters
const ch = chat({ threadId: "abc123" });
ch.name; // "chat:abc123"
ch.message; // TopicRef for "message" on "chat:abc123"
ch.message.channel; // "chat:abc123"
ch.message.topic; // "message"
Parameterized channels isolate subscribers to a specific instance. A subscriber to chat:abc123 won't receive messages published to chat:def456.
Topic schemas
Every topic requires a schema property. The schema serves two purposes:
- Type inference: TypeScript infers the data type for publishing and subscribing
- Runtime validation: data is validated against the schema when publishing and (optionally) when subscribing
Standard Schema libraries
Any library implementing the Standard Schema spec works: Zod, Valibot, ArkType, and others.
import { z } from "zod";
const ch = realtime.channel({
name: "pipeline",
topics: {
status: { schema: z.object({ message: z.string() }) },
},
});
staticSchema<T>()
For type-only schemas with zero runtime validation cost. Useful when you trust the data source and only want compile-time type checking.
import { realtime, staticSchema } from "inngest";
const ch = realtime.channel({
name: "metrics",
topics: {
usage: { schema: staticSchema<{ tokens: number; latencyMs: number }>() },
},
});
You can mix staticSchema and runtime schemas on the same channel:
const ch = realtime.channel({
name: "pipeline",
topics: {
// Runtime validation with Zod
status: { schema: z.object({ message: z.string() }) },
// Type-only, no validation cost
usage: { schema: staticSchema<{ tokens: number }>() },
},
});
Topic accessors
Each topic defined on a channel becomes a property on the channel instance. These accessors return a TopicRef, a lightweight reference carrying the channel name, topic name, and schema config.
const ch = chat({ threadId: "abc123" });
// Each accessor is a TopicRef
ch.message.channel; // "chat:abc123"
ch.message.topic; // "message"
ch.message.config; // { schema: ... }
// Pass topic refs to publish and subscribe
await publish(ch.message, { text: "Hello!", sender: "alice" });
Topic refs are the primary way to reference a specific topic on a specific channel instance. They're used by publish, step.realtime.publish, and getClientSubscriptionToken.
Type inference
Channel definitions expose type utilities for extracting topic data types and parameter types.
const pipeline = realtime.channel({
name: ({ runId }: { runId: string }) => `pipeline:${runId}`,
topics: {
status: { schema: z.object({ message: z.string() }) },
tokens: { schema: staticSchema<{ token: string }>() },
},
});
// Infer topic data types
type StatusData = typeof pipeline.$infer.status; // { message: string }
type TokenData = typeof pipeline.$infer.tokens; // { token: string }
// Infer channel parameters (parameterized channels only)
type Params = typeof pipeline.$params; // { runId: string }
Portability
Channel definitions are plain objects with no server-side dependencies. Define them in a shared file and import them on both server and client:
import { realtime, staticSchema } from "inngest";
import { z } from "zod";
export const pipelineChannel = realtime.channel({
name: ({ runId }: { runId: string }) => `pipeline:${runId}`,
topics: {
status: { schema: z.object({ message: z.string() }) },
tokens: { schema: staticSchema<{ token: string }>() },
},
});
Import from your function code, your API routes (for token minting), and your React components. The same definition provides type safety everywhere.