Flue is a framework because deployment is part of the contract. The runtime is not only session.prompt(...). It is also the generated server or worker that loads agents, configures stores, creates sandboxes, registers providers, mounts flue(), and exposes a stable inspection surface.
The same user agent source can become a Node server or a Cloudflare Worker. The agent handler stays recognizable; the generated runtime shape changes because the host changes the available bindings and stores.
The source pin for this chapter is withastro/flue@dbaa9effa305561c627c6836559f8a0cbce67875.
Build targets matter because the generated entry wires runtime configuration, stores, bindings, and routes before requests arrive.
Domain Word
In Flue, a build target is a generated runtime shape such as Node or Cloudflare. It is not an example folder. It is the part of the framework that adapts the same agent source to a host.
The invariant is: deployment target changes adapters and bindings, not the core harness language.
The twelve-factor pressure is build, release, run and dev/prod parity. Build-time discovery, release artifacts, and runtime invocation have different jobs, but they should preserve the same concepts.
Build Context
packages/cli/src/lib/build.ts is the entry point. It discovers the project root, agents, roles, optional app entry, output directory, and target plugin. It emits a manifest, generates a target-specific entry point, and either bundles with esbuild or writes a pass-through entry for a downstream bundler.
project root ├─ .flue/agents/*.ts ├─ optional roles and app.ts └─ flue.config.* │ ▼ build.ts ├─ resolve config ├─ discover agents ├─ create manifest └─ call target plugin │ ├─ Node plugin -> bundled server.mjs └─ Cloudflare plugin -> _entry.ts + wrangler.jsonc
The manifest is the bridge between source discovery and runtime configuration. Generated code uses it to configure the runtime before requests arrive.
Config Lifecycle
packages/cli/src/lib/config.ts is explicit about config scope. flue.config.* handles project-level build settings such as target, root, and output. The comments also say provider/model configuration belongs in app.ts, where runtime environment is available.
That separation matters:
| Phase | Belongs here |
|---|---|
| Build config | Target, root, output, config-file discovery. |
| Runtime app | Provider registration, provider settings, middleware, app composition. |
| Agent handler | Work declaration and calls into the harness. |
Secrets and platform bindings are runtime concerns. A build config should not pretend it has the same lifecycle as a deployed Worker request or Node process.
Node Generated Entry
packages/cli/src/lib/build-plugin-node.ts generates a Node/Hono runtime. The generated entry imports the discovered agent handlers, runtime helpers, stores, registry, local session env support, and optional user app.
The Node entry does a few load-bearing things before serving:
| Step | Why |
|---|---|
| Build handler map and webhook agent list | Lets public routes validate and dispatch agents. |
| Create in-memory run registry and subscribers | Supports bare run lookup and live stream tailing in-process. |
| Configure local session env | Gives Node a host-backed execution/filesystem boundary. |
Call configureFlueRuntime(...) | Seeds flue() before routes handle requests. |
| Mount default app or user app | Lets simple projects run without custom Hono code. |
The generated entry’s job is intentionally narrow: import handlers, build runtime config, instantiate sandboxes/stores, and start the listener.
Cloudflare Generated Entry
packages/cli/src/lib/build-plugin-cloudflare.ts generates a Worker entry. It differs from Node because Cloudflare has request-scoped environment bindings, Durable Objects, Workers AI bindings, and Wrangler as the downstream bundler.
The Cloudflare plugin:
- skips Flue’s esbuild pass and lets Wrangler bundle
- writes
_entry.ts - merges or creates
wrangler.jsonc - registers the Cloudflare provider if needed
- detects user sandbox bindings
- wires request-specific session/run stores, using Durable Object storage when available
- configures a request-specific run registry
- exports per-agent Durable Object classes
That is a lot of code, but it is all target adaptation. The public concepts stay the same: agents, harness, sessions, runs, registry, stream routes.
Target Fan-Out
| Concern | Node target | Cloudflare target |
|---|---|---|
| Server shape | Hono server served by @hono/node-server | Worker fetch handler |
| Bundling | Flue/esbuild bundle | Wrangler bundles generated entry |
| Session store | In-memory/local process defaults | Request-scoped store; DO SQLite when available |
| Run registry | In-memory registry | Request-scoped registry; Durable Object aware |
| Sandbox | Local session env or user app composition | Cloudflare sandbox/bindings |
| Provider binding | Runtime app or env lookup | Workers AI binding registration path |
| Config output | bundled server artifact | _entry.ts and wrangler.jsonc |
The target changes the plumbing. It should not change how a reader understands a session or run.
app.ts Composition
Both plugins account for optional user app composition. If no app.ts exists, generated code builds a default app that mounts flue() at root. If a user app exists, generated code imports it and passes the listener through.
This is the right compromise. Simple projects get a working service. Advanced projects can mount flue() and admin() behind their own middleware, auth, paths, and observability.
Why This Is Not An Example Folder
Example folders teach usage. Build targets carry product behavior.
If a target forgets to configure the run registry, GET /runs/:runId breaks. If Cloudflare generation forgets the Durable Object backed storage path, session persistence semantics change when requests route through agent objects. If Node generation starts before configureFlueRuntime(...), public routes can exist without a configured runtime.
Those are framework bugs, not example bugs.
What Breaks If This Boundary Drifts
| Drift | Failure |
|---|---|
| Build config owns runtime secrets | Providers work locally and fail at deploy time. |
| Generated entry starts before runtime configuration | flue() routes exist but cannot dispatch or inspect runs. |
| Node assumptions leak into Cloudflare | In-process registries replace Durable Object backed lookup. |
| Cloudflare assumptions leak into Node | Simple local runs require platform bindings they should not need. |
| Build targets are treated as samples | Deployment regressions stop being tested as framework behavior. |
What To Copy
The copyable pattern is target-specific generation around target-independent vocabulary. Keep “agent”, “harness”, “session”, “run”, and “registry” stable, then adapt stores, bindings, sandbox creation, and server boot for the host.
That lets documentation, SDKs, and tests talk about one framework even when deployment artifacts look different.
Verify In Source
build.tsdiscovers agents, writesmanifest.json, asks a target plugin for entry code, and handles bundle strategy.config.tslimitsflue.config.*to build-level target/root/output concerns.build-plugin-node.tsgenerates a Hono server and callsconfigureFlueRuntime(...).build-plugin-node.tscreates in-memory run registry/subscriber support for Node.build-plugin-cloudflare.tswrites_entry.ts, creates/mergeswrangler.jsonc, and defers bundling to Wrangler.build-plugin-cloudflare.tsregisters Cloudflare provider support and wires Durable Object backed runtime pieces.- Both plugins support optional user app composition.