zudo-slack-wisdom
GitHub repository

Type to search...

to open search from anywhere

Getting Started on Cloudflare

A minimal Flue 2.0.3 Vite and Cloudflare Workers setup with generated Durable Objects

Versioned Starting Point

Use Node.js >=22.19.0, a Vite-based project, and one reviewed Flue release. This guide targets Flue 2.0.3; check Source Map and Versioning before upgrading.

npm install @flue/runtime@2.0.3 hono valibot
npm install -D @flue/cli@2.0.3 @flue/vite@2.0.3 @cloudflare/vite-plugin vite@^8 wrangler@^4.120.0

Verified peer floors

Installing the toolchain against a working repository pinned three floors the tagged prose does not state, all confirmed by building workers/agent-server inzudolab/zudo-text#4625:

  • @flue/vite@2.0.3 peer-depends on vite@^8.0.0 — not 6, not 7. A monorepo whose root is on an older Vite cannot share it with the Flue worker; the worker carries its own.

  • @cloudflare/vite-plugin@1.51.x wants wrangler@^4.120.0, which is ahead of many existing repositories' pin.

  • Every @flue/* manifest enforces Node.js >=22.19.0, so a repository-wide engines.nodebelow that fails installation, not just the Flue package.

Also worth stating because they are the names one would guess: @flue/vite-plugin and @flue/agentdo not exist on npm. The published packages are @flue/runtime, @flue/cli, @flue/vite, and@flue/slack.

The plugin order is a contract. flue() scans and configures the application before the Cloudflare plugin consumes flueWorkerConfig().

vite.config.ts
import { cloudflare } from '@cloudflare/vite-plugin';
import { flue, flueWorkerConfig } from '@flue/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [flue({ providers: ['cloudflare'] }), cloudflare({ config: flueWorkerConfig() })],
});

Keep that exact order and form. The tagged 2.0.3 Cloudflare deployment prose omits flueWorkerConfig(), but the matching configuration contract, changelog, and runnable example require it — and a real build settles it: the runnable-example form is the one that builds. The source map records the discrepancy.

Verified: providers is the bundle-size lever, and the default overshoots the free tier

With providers unset, Flue registers every built-in pi-ai provider and the Worker uploads at1249 KB gzipped — Anthropic, OpenAI, Google Vertex, Mistral, Azure, and an OpenTelemetry chunk, none of them reachable from a Workers-AI-only Worker. Settingflue({ providers: ['cloudflare'] }) cuts the same Worker to 783 KB gzipped, a 37% reduction, with all bindings intact; 'cloudflare' is the Workers AI binding provider and is explicitly filtered out of the pi-ai import list, so it costs nothing to name. This matters because the free Workers tier caps an upload at 1 MB gzipped, which the default configuration exceeds. The option is documented only in the FlueConfig.providers JSDoc, not in the Cloudflare getting-started prose. Measured in zudolab/zudo-text#4625. List a provider id here only when this Worker actually dispatches to it.

Generated Agents and Authored Configuration

Each registered agent function generates a Durable Object class and binding. For example, SupportChat produces FlueSupportChatAgent and env.FLUE_SUPPORT_CHAT_AGENT. Do not hand-author generated FLUE_* bindings. Flue merges those bindings and its generated Worker entry into generated configuration while leaving the application-owned wrangler.jsonc intact.

Verified: the naming rule, and why to read it back anyway

The transformation is agentClassName() producing `Flue${PascalCase}Agent` andagentBindingName() splitting camel boundaries into FLUE_<SNAKE_UPPER>_AGENT — read from the plugin source and confirmed twice against a built dist/<worker>/wrangler.json, where an agent named ZudoAssistant produced class FlueZudoAssistantAgent and bindingFLUE_ZUDO_ASSISTANT_AGENT. Predicting the name is reliable; verifying it from the build output is still the right habit, because that generated file is what the migrations tag has to agree with.

Declare platform settings, application-owned bindings, and the ordered migration history for generated classes:

wrangler.jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "support-agent",
  "compatibility_date": "2026-06-01",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["FlueSupportChatAgent"]
    }
  ],
  "vars": {
    "PUBLIC_APP_ORIGIN": "https://app.example.com"
  }
}

The cloudflare/... model selected in the agent example requires that Workers AI binding. Flue's generated configuration does not create application provider bindings for you; declare the binding or configure another reviewed model provider and its server-side credential.

Migration history is ordered and append-only. Add a migration when adding, renaming, or removing a deployed generated class; use Wrangler's new_sqlite_classes, renamed_classes, or deleted_classes as appropriate. Changing an app.ts route path is not a storage migration.

These are Durable Object class migrations, not application database schema migrations. Run D1 or external-database schema migrations through the datastore's own migration system. Likewise, application-owned Durable Objects, R2, Queues, D1, and service bindings must be declared explicitly; calling them from a Flue tool does not make them conversation storage.

Durable Object storage can survive activation, but ordinary JavaScript instance fields do not survive eviction. Persist required state in Durable Object storage or another explicit persistence service.

Secrets, Variables, and Environments

Use vars only for non-secret configuration such as PUBLIC_APP_ORIGIN; bindings make resource access explicit. Put Slack tokens, signing secrets, provider API keys, and other credentials in Worker secrets, server-side. Never place them in vars, browser-shipped code, or committed local files. For Slack-specific handling, use the existing Secrets and Config guide.

Named Wrangler environments are separate deployment configurations. Before using --env staging or --env production, review every binding and variable: non-inheritable settings including vars must be redeclared for that environment, and each environment needs its intended secrets. Treat a named environment as a configuration review, not a name suffix.

Pin compatibility_date deliberately. Advancing it can change runtime behavior, so make it a reviewed change, run relevant tests, and add only flags the deployment needs. Test required flags too; Flue 2.0.3 requires nodejs_compat for its Cloudflare runtime. Consult Cloudflare's compatibility-date documentation for current platform behavior.

Verified: compatibility_date has a hard floor of 2026-04-01

MIN_COMPATIBILITY_DATE in the Vite plugin throws the build below that date, citing SQLite-backed Durable Objects, nodejs_compat v2, and AsyncLocalStorage. This is the fastest way to fail when adding a Flue Worker to a repository whose other Workers sit on an older date — copying a sibling's configuration fails immediately rather than subtly. Hit and confirmed inzudolab/zudo-text#4625.

One generated-configuration quirk worth knowing before wiring D1 into a Flue Worker: the merge injects migrations_dir: "../../migrations" into the D1 binding. That path is relative to the build output directory, not the package root, so it is harmless while wrangler d1 migrations apply is never run from this package and surprising the moment it is.

Minimal Local Commands

Use Vite commands, not removed flue dev or flue build:

npx vite dev
npx vite build
npx wrangler deploy

Verified: the build is not optional before the deploy, and the ordering is load-bearing

wrangler.jsonc has no mainflue() generates the Worker entry — so wrangler deploy cannot resolve anything on its own. What makes the bare command work is that vite build writes.wrangler/deploy/config.json, a one-line redirect ({"configPath":"../../dist/<worker>/wrangler.json"}) that wrangler follows to the generated configuration. The tagged guide's bare npx wrangler deploy is therefore accurate but silently depends on a build having run in the same checkout. A CI job that deploys without a preceding build step fails confusingly; make the build an explicit prerequisite of the deploy job. Confirmed inzudolab/zudo-text#4625.

The final command deploys; it was not run for this documentation work. Before deployment, review the target Worker name, account, environment, secrets, bindings, migrations, compatibility date, and flags. A local build validates configuration shape, not deployed bindings or secret values.

One testing consequence of this build model is worth planning for. createAgentRouter() resolves its agent identity from the function name at import time and calls requireRuntime() only when a route is actually invoked, so an authored app.ts imports cleanly into a plain Vitest run with no Flue plugin and no Miniflare-plus-Flue harness. What such a test cannot do is execute an agent route — that throws because the runtime was never configured. Assert the routing decision (a mounted path reaches the router, an unmounted path 404s) rather than a response body, and cover the agent's behavior at the tool layer and through live evaluations instead. Testing and Operations covers the rest of that split.

Safe Upgrade Loop

  1. Inspect the installed release: npx flue docs search "Cloudflare Vite" and npx flue docs read reference/configuration.

  2. Compare it with the matching tagged changelog, runtime contract, and runnable example—not an unversioned snippet alone.

  3. Upgrade related @flue/* packages in one reviewed lockfile change unless the changelog explicitly supports a mixed release.

  4. Re-check generated Durable Object class names, append the required Wrangler migration, and run a local Vite build before deployment.

Flue conversation durability does not replace canonical business records, application schema migrations, or a general filesystem. Core Concepts and API explains those boundaries.

Revision History

CreatedUpdated