# Channels and topics

> Give every realtime update a clear destination and a predictable shape.

A **channel** decides where a message goes, and a **topic** decides its shape. Define both once and share the definition between your function, your token route, and your UI. TypeScript then checks every publish and every subscription against the same types.

For example, a document import can publish `status` and `result` messages to a channel for that document. Only clients watching that document receive them.

## Define a channel and its topics

Use `realtime.channel()` from `inngest`. Give the channel a name and give every topic a `schema`.

```typescript {{ title: "TypeScript" }}
import { realtime } from "inngest";
import { z } from "zod";

export const documentChannel = realtime.channel({
  name: ({ documentId }: { documentId: string }) =>
    `document:${documentId}`,
  topics: {
    status: {
      schema: z.object({
        message: z.string(),
        progress: z.number(),
      }),
    },
    result: {
      schema: z.object({
        url: z.string().url(),
      }),
    },
  },
});
```

Channels and topics are plain strings in Python. Pass them to `realtime.publish()` and `realtime.get_subscription_token()`.

Channels and topics are plain strings in Go. Pass them to `realtime.Publish()` and encode payloads yourself, for example with `json.Marshal`.

Call the factory with a document ID to get a channel instance. Its `status` and `result` properties are **topic references**. You pass a topic reference to a publish call, and it carries the channel name, topic name, and schema.

```typescript {{ title: "TypeScript" }}
const document = documentChannel({ documentId: "abc123" });

const channelName = document.name;
const statusChannel = document.status.channel;
const statusTopic = document.status.topic;
```

Channels and topics are plain strings in Python. Pass them to `realtime.publish()` and `realtime.get_subscription_token()`.

Channels and topics are plain strings in Go. Pass them to `realtime.Publish()` and encode payloads yourself, for example with `json.Marshal`.

A message published to `document:abc123` reaches only subscribers of `document:abc123`. A subscriber of `document:def456` never sees it.

## Fixed and parameterized channels

Use a **fixed name** for a shared stream such as system alerts. `realtime.channel()` returns the channel instance directly.

```typescript {{ title: "TypeScript" }}
export const alertsChannel = realtime.channel({
  name: "system:alerts",
  topics: {
    alert: {
      schema: z.object({ message: z.string() }),
    },
  },
});
```

Channels and topics are plain strings in Python. Pass them to `realtime.publish()` and `realtime.get_subscription_token()`.

Channels and topics are plain strings in Go. Pass them to `realtime.Publish()` and encode payloads yourself, for example with `json.Marshal`.

Use a **name function** when each document, session, job, or user needs its own stream. Pass your app's identifier when you publish or subscribe. The identifier is yours to choose; it doesn't have to be an Inngest run ID.

Follow these rules when you name channels:

- **Keep names stable** for the lifetime of the work. Use IDs your app already has.
- **Group what one view needs.** A subscription token covers one channel, so put the topics a single page shows on the same channel.
- **Leave secrets and personal data out.** A channel name routes messages; it does **not** decide who may read them.

## Choose a topic schema

A [Standard Schema](https://github.com/standard-schema/standard-schema) validator such as Zod, Valibot, or ArkType gives you TypeScript types and validates each payload when you publish. Subscribers also validate incoming messages by default. Use a runtime schema when a malformed message must fail.

For type checking without runtime validation, use `staticSchema<T>()`:

```typescript {{ title: "TypeScript" }}
import { realtime, staticSchema } from "inngest";

export const metricsChannel = realtime.channel({
  name: "metrics",
  topics: {
    usage: {
      schema: staticSchema<{ tokens: number; latencyMs: number }>(),
    },
  },
});
```

Channels and topics are plain strings in Python. Pass them to `realtime.publish()` and `realtime.get_subscription_token()`.

Channels and topics are plain strings in Go. Pass them to `realtime.Publish()` and encode payloads yourself, for example with `json.Marshal`.

`staticSchema<T>()` skips runtime validation, which suits small, high-volume payloads such as streamed tokens. You can mix runtime and type-only schemas on different topics in the same channel.

## Share the definition

Put the channel definition in a shared module, such as `src/inngest/channels.ts`. Import it in your function, in the server code that mints subscription tokens, and in the component that subscribes. Topic names and payload types then stay aligned everywhere.

Use a topic reference to publish to a specific channel and topic:

```typescript {{ title: "TypeScript" }}
import { inngest } from "./client";
import { documentChannel } from "./channels";

const document = documentChannel({ documentId: "abc123" });

await inngest.realtime.publish(document.status, {
  message: "Analyzing",
  progress: 50,
});
```

```python {{ title: "Python" }}
from inngest.experimental import realtime

from .client import inngest_client

async def report_progress() -> None:
    # Channels and topics are plain strings in Python.
    await realtime.publish(
        client=inngest_client,
        channel="document:abc123",
        topic="status",
        data={"message": "Analyzing", "progress": 50},
    )
```

```go {{ title: "Go" }}
import (
	"context"
	"encoding/json"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/realtime"
	"github.com/inngest/inngestgo/step"
)

type DocumentUploaded struct {
	DocumentID string `json:"documentId"`
}

func Register(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "process-document"},
		inngestgo.EventTrigger("app/document.uploaded", nil),
		func(ctx context.Context, input inngestgo.Input[DocumentUploaded]) (any, error) {
			// Channels and topics are plain strings in Go.
			channel := "document:" + input.Event.Data.DocumentID

			return step.Run(ctx, "analyze", func(ctx context.Context) (any, error) {
				data, err := json.Marshal(map[string]any{
					"message":  "Analyzing",
					"progress": 50,
				})
				if err != nil {
					return nil, err
				}
				// realtime.Publish works only inside an Inngest function.
				return nil, realtime.Publish(ctx, channel, "status", data)
			})
		},
	)
}
```

This immediate publish suits frequent progress updates. For a state change that a retry must not publish twice, use `step.realtime.publish()` inside the function. See [Durable and immediate publishing](/docs-markdown/realtime/overview#durable-and-immediate-publishing).

## Authorize subscriptions

A token limits a client to one channel and the topics you list. Before your server calls `getClientSubscriptionToken()`, check that the current user may read the requested document or job. Include only the topics that user should receive.

[Subscription tokens](/docs-markdown/realtime/guides/subscription-tokens) shows the full authorization flow.

## Next steps

- [Subscription tokens](/docs-markdown/realtime/guides/subscription-tokens) shows how to authorize subscribers.
- [Guides](/docs-markdown/realtime/guides) cover publishing and subscription patterns.
- [Channels reference](/docs-markdown/reference/typescript/v4/realtime/channels) lists every channel option.