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 | codex

When 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 | codex

Ownership 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

ConceptPractical meaningOwner
src/app.tsOptional authored Hono application entrypoint that can mount flue() explicitlyApplication
flue()Mountable Flue sub-application from @flue/runtime/routingFlue runtime
/channels/<name>/...Public namespace for a discovered channel fileFlue runtime and channel package
channels/<provider>.tsProvider ingress module discovered by filenameApplication
export const channelNamed binding that exposes the Flue channel integrationApplication module
Provider SDK clientOutbound API client such as Slack WebClientApplication
Signature verificationProvider-specific request authenticationChannel package or custom channel
Business authorizationChecks such as workspace, repository, user, tenant, or resource accessApplication

Relevant Source Files

  • apps/docs/src/content/docs/guide/channels.md — Defines the reader-facing channel model, the flue add channel workflows, the Slack module example, the inbound-versus-outbound ownership boundary, and file-based routing examples for src/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.