Channels Routing
Purpose and Scope
Channels routing is the part of a Flue application that turns outside HTTP events into durable application work. In practice, it covers two related concerns. First, provider-specific channel packages receive webhooks, verify the request, parse provider-native payloads, and call application handlers. Second, the Flue HTTP application exposes discovered channel modules under predictable route namespaces alongside agents and workflows. This page explains how to think about those pieces together so you can add provider ingress without confusing it with outbound provider clients or ordinary application routes.
Sources: apps/docs/src/content/docs/guide/channels.md
The most important boundary is that a channel is inbound HTTP infrastructure, not a replacement SDK for Slack, GitHub, Stripe, or another provider. The Channels guide states that channel packages focus on verification, parsing, protocol handshakes, body handling, typed payloads, and discovered routes. Your application remains responsible for outbound SDK clients, credentials, OAuth installation state, token rotation, authorization policy, delivery deduplication, and business persistence. That separation keeps Flue small and lets each provider ecosystem keep its own client library semantics.
Sources: apps/docs/src/content/docs/guide/channels.md
Core Routing Model
A Flue project can expose public routes without an authored HTTP entrypoint, or it can let you provide one. The official routing flow is that, when src/app.ts is absent, Flue generates an application that mounts the Flue public API at /. When src/app.ts exists, its default export owns the request pipeline and must explicitly mount flue() from @flue/runtime/routing. The authored application is an ordinary Hono app, so you can add middleware, health checks, route prefixes, and custom application endpoints before or around the Flue mount.
The mount point matters because Flue routes are relative to where you attach flue(). Mounting app.route('/', flue()) publishes the conventional route tree, including channels below /channels/..., agents below /agents/..., workflow invocation below /workflows/..., and run streams below /runs/.... Mounting under a prefix changes the external URL while preserving the same internal resource layout below that prefix. This lets teams put Flue behind a broader service router without changing channel module names or agent definitions.
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
const app = new Hono();
app.get('/health', (c) => c.json({ ok: true }));
app.route('/', flue());
export default app;Channel Modules and File-Based Routing
Channel modules are discovered by convention. Each immediate file beneath channels/ exports one named channel binding, and the filename defines the route namespace. The Channels guide uses examples like src/channels/github.ts -> /channels/github/webhook and src/channels/slack.ts -> /channels/slack/events. The suffix after the namespace is provider-specific: Slack might expose an events handler, while GitHub might expose a webhook handler. The route name comes from the file; the method, suffix, verification behavior, and parsed callback shape come from the channel implementation.
Sources: apps/docs/src/content/docs/guide/channels.md
A typical first-party module keeps two concerns side by side without merging them. The named channel export is the Flue integration that publishes verified inbound handlers. A separate named client export can initialize the provider SDK with outbound credentials, such as a Slack WebClient using SLACK_BOT_TOKEN. Application code, tools, or agents may use that client later, but the channel itself does not become a universal outbound API surface. This pattern makes route discovery predictable while keeping operational ownership clear.
import { createSlackChannel } from '@flue/slack';
import { WebClient } from '@slack/web-api';
export const client = new WebClient(process.env.SLACK_BOT_TOKEN);
export const channel = createSlackChannel({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
// Path: /channels/slack/events
async events({ payload }) {
if (payload.type !== 'event_callback') return;
// Handle payload.event using Slack's native types and fields.
},
});Adding First-Party and Custom Channels
For first-party integrations, the documented workflow starts with flue add. The command gives a coding agent the integration blueprint for a channel package and lets it inspect the project before creating a module such as src/channels/slack.ts. The generated code should preserve the provider’s native event shape, configure the channel package’s verification inputs, and leave outbound operations to the provider SDK. In the Slack example, the handler receives payload, checks payload.type, and then works with Slack-native fields rather than a generic event abstraction.
Sources: apps/docs/src/content/docs/guide/channels.md
flue add channel slack --print | codexWhen Flue does not provide a first-party channel, the guide recommends giving flue add the provider’s webhook documentation and selecting the generic channel blueprint. That workflow is intended to produce a discovered channels/<provider>.ts module that verifies the unconsumed request body, preserves the provider-native event, and adds the provider’s established SDK for outbound calls. A custom channel should be tested like any other ingress boundary: valid signatures, invalid signatures, handshake requests, response expectations, body parsing, and differences between the configured Node or Cloudflare target all matter.
Sources: apps/docs/src/content/docs/guide/channels.md
flue add channel https://provider.example/webhooks --print | codexOwnership and Security Boundaries
Routing is not just URL placement; it is also an authorization boundary. Channel packages own provider request authentication and signature verification because they know the provider protocol. Flue owns the discovered route beneath /channels/<name>/... because it knows how to load channel modules into the application. Your application owns what happens after the provider event is accepted: whether to dispatch to an agent, invoke workflow or application code, store deduplication markers, call outbound APIs, or reject a business action because the event is not authorized for the selected resource.
Sources: apps/docs/src/content/docs/guide/channels.md
This boundary also affects custom middleware. In an authored src/app.ts, broad checks such as user authentication, service-level headers, IP allowlists, or shared logging can run before the Flue mount or on route groups like /channels/*. Provider signature verification should still happen inside the channel package or channel module, because generic middleware usually cannot safely parse provider-specific signatures after consuming the request body. If access depends on a selected resource, add that check at the application layer after the provider event is known and before dispatching durable work.
The same principle applies when channels deliver work to agents or workflows. The channel proves that the provider sent the request; it does not prove that every requested operation is allowed. For example, a Slack event handler may dispatch a task to an agent only after checking workspace, channel, user, installation, and application policy. A GitHub webhook may be authentic but still reference a repository that should not trigger the selected agent. Treat provider verification as ingress authentication, and treat business authorization as application policy.
Execution Flow
A routed provider event usually follows a short but important sequence. The provider sends an HTTP request to a URL under the mounted Flue application. Flue selects the discovered channel by namespace, such as slack from src/channels/slack.ts. The channel implementation verifies the request using provider-specific inputs, parses the body into typed provider-native data, and invokes the handler you exported in the channel configuration. The handler can then perform ordinary application work, return a provider-specific response, or enqueue durable work by dispatching to an agent or invoking a workflow.
Sources: apps/docs/src/content/docs/guide/channels.md
Because channel handlers are application code, they are a good place to normalize provider events into your own domain language before invoking durable work. Keep that normalization narrow. Preserve the original provider payload when it is useful for auditability, debugging, or downstream tools, but do not force every channel into one lowest-common-denominator schema. The guide explicitly favors provider-native data so integrations can use each provider’s documented event model and SDK types. This makes handlers easier to compare with provider documentation and easier to test with real webhook fixtures.
A useful mental model is that /channels/<name>/... is the receiving dock, while agents and workflows are the work floor. The receiving dock checks that the shipment is legitimate and opens it according to the provider’s protocol. The work floor decides what the organization should do with it. In code, that means channel modules should be small, deterministic, and security-conscious, while agents, workflows, tools, and storage adapters should hold the application-specific behavior that follows from accepted events.
Compact Reference
| Concept | Practical meaning | Owner |
|---|---|---|
src/app.ts | Optional authored Hono application entrypoint that can mount flue() explicitly | Application |
flue() | Mountable Flue sub-application from @flue/runtime/routing | Flue runtime |
/channels/<name>/... | Public namespace for a discovered channel file | Flue runtime and channel package |
channels/<provider>.ts | Provider ingress module discovered by filename | Application |
export const channel | Named binding that exposes the Flue channel integration | Application module |
| Provider SDK client | Outbound API client such as Slack WebClient | Application |
| Signature verification | Provider-specific request authentication | Channel package or custom channel |
| Business authorization | Checks such as workspace, repository, user, tenant, or resource access | Application |
Relevant Source Files
apps/docs/src/content/docs/guide/channels.md— Defines the reader-facing channel model, theflue add channelworkflows, the Slack module example, the inbound-versus-outbound ownership boundary, and file-based routing examples forsrc/channels/...modules.
Next Steps
After wiring a channel route, test it as an ingress boundary rather than only as a happy-path callback. Send valid and invalid signatures, provider handshake requests, malformed payloads, duplicate deliveries, and events that should be authenticated but not authorized for the selected application resource. Then connect the accepted event to the rest of Flue deliberately: dispatch to an agent when autonomous work is appropriate, invoke a workflow when the process is structured, or call ordinary application code when no durable AI work is needed.
For deeper implementation work, read the routing API material for app.ts, Fetchable, and flue(), then pair this page with provider-specific ecosystem pages. Those pages explain the package-level details for Slack, Discord, GitHub, Google Chat, Teams, Messenger, WhatsApp, Telegram, Twilio, Intercom, Linear, Zendesk, and similar integrations. If you are building a provider that Flue does not ship, start from the generic channel blueprint and keep the same ownership split: channel for verified inbound HTTP, provider SDK for outbound calls, and application code for policy and durable behavior.