@fluojs/platform-cloudflare-workers
v2.1.2
Published
Cloudflare Workers HTTP adapter for the Fluo runtime built on the shared Web Request/Response core.
Maintainers
Readme
@fluojs/platform-cloudflare-workers
Cloudflare Workers HTTP adapter for the fluo runtime, optimized for the edge.
Table of Contents
- Installation
- When to Use
- Quick Start
- Common Patterns
- Lifecycle and Public Seam Notes
- Conformance Coverage
- Public API Overview
- Related Packages
- Example Sources
Installation
npm install @fluojs/platform-cloudflare-workersThis package is intended to run on Cloudflare Workers. The published manifest intentionally does not declare engines.node, so npm metadata stays aligned with the Workers runtime contract; the repository's Node.js >=24.11.0 <27 requirement only applies to the maintainer build/test toolchain.
When to Use
Use this package when deploying fluo applications to Cloudflare Workers. It is designed for the serverless edge environment, providing a lightweight fetch-based adapter that respects Worker isolate constraints and native Web APIs.
The adapter binds each request lifecycle to executionContext.waitUntil(...) after the dispatcher is bound and keeps in-flight dispatches, WebSocket upgrades through the upgraded server socket's terminal close, and SSE (text/event-stream) response bodies alive during close() so Worker shutdown does not drop active work mid-request.
During application shutdown, the adapter stops accepting new ingress immediately and gives active HTTP handlers a bounded 10-second drain window before close() fails with a timeout instead of hanging indefinitely. While that drain is still in progress, a concurrent listen() call rejects with Cloudflare Workers adapter cannot listen while shutdown is still draining. instead of reopening the Worker. Once closed, follow-up HTTP and WebSocket upgrade requests receive the same JSON 503 shutdown response until the adapter is explicitly listened again. Lazy hosts keep returning shutdown responses while a timed-out close is still draining, but they clear that temporary gate once the underlying close eventually settles so a later request can bootstrap a fresh Worker application.
Quick Start
These entrypoint fragments belong in an existing Workers project. They require AppModule from the application-owned ./app.module, the @fluojs/runtime dependency, decorator build configuration, and Worker deployment configuration. Retain the module composition in Application Bootstrap Protocol and the host ownership contracts below.
Direct Application Creation
Create the concrete adapter first, then let FluoFactory.create own application
creation and call app.listen() to bind its dispatcher without opening a socket.
import { FluoFactory } from '@fluojs/runtime';
import { CloudflareWorkerHttpApplicationAdapter } from '@fluojs/platform-cloudflare-workers';
import { AppModule } from './app.module';
const adapter = CloudflareWorkerHttpApplicationAdapter.create();
const app = await FluoFactory.create(AppModule, { adapter });
await app.listen();
export default {
fetch: (req, env, ctx) => adapter.fetch(req, env, ctx),
};Host-Owned Lazy Application
Use a CloudflareWorkerApplicationHost only when this isolate must own lazy
bootstrap, generation replacement, retry, and close/drain recovery. It is a
different capability from direct application creation, not a second Factory.
import { CloudflareWorkerApplicationHost } from '@fluojs/platform-cloudflare-workers';
import { AppModule } from './app.module';
const worker = CloudflareWorkerApplicationHost.create(AppModule);
export default {
fetch: worker.fetch,
};Close Ownership and Lazy Restart
Cloudflare Workers does not provide a host-invoked shutdown callback to the exported fetch handler. When migrating NestJS shutdown hooks, choose an application-owned close trigger. An out-of-band lifecycle trigger, running outside any worker.fetch invocation, may call await worker.close() directly. A management route handled inside the same worker.fetch invocation must return its current response without awaiting close(), then observe it with executionContext.waitUntil(worker.close()) or an equivalent non-self-awaiting mechanism; otherwise close() waits for that active request to drain and reaches the shutdown timeout. Exporting worker.fetch alone does not arrange a close call.
A successful worker.close() is intentionally restartable. It releases the
current host generation; a later worker.fetch(...) creates a fresh generation
in the isolate, rerunning bootstrap lifecycle hooks and reconstructing application
singleton providers. For an env-aware host, that generation uses the cached
configuration from the first environment without calling its factory again. Do
not treat close() as a terminal Worker shutdown signal. If an application needs
terminal behavior, it must own and enforce that state explicitly.
Env-Aware Lazy Host
Use the { fromEnv } overload of the same CloudflareWorkerApplicationHost.create
method when the first Worker env must select the root module or bootstrap
options. The factory runs once per host instance before module registration; its
configuration remains cached while each running application generation reuses it.
import { CloudflareWorkerApplicationHost } from '@fluojs/platform-cloudflare-workers';
import { createAppModule } from './app.module';
interface WorkerEnv {
API_PREFIX: string;
DB: D1Database;
}
const worker = CloudflareWorkerApplicationHost.create<WorkerEnv>({
fromEnv: (env) => ({
rootModule: createAppModule({ database: env.DB }),
options: {
globalPrefix: env.API_PREFIX,
},
}),
});
export default {
fetch: worker.fetch,
};worker.ready(env) requires an explicit env for the same reason. The first
supplied environment determines the singleton bootstrap configuration; later
request environments still attach to request.cloudflare.env, but do not
reconfigure it. A successful worker.close() releases only the current
application generation: the next ready(env) or fetch(...) creates a fresh
application from the retained first-environment module and options without
calling the factory again. Use CloudflareWorkerApplicationHost.create(AppModule,
options) when bootstrap configuration is already available before the first
Worker request.
For the fixed-module host's request-bound env path, fetch-time bindings cannot
supply ConfigModule.forRoot(...) or singleton bootstrap providers. Read,
validate, and narrow request-varying bindings, then pass application-shaped
values to provider methods. Choose the { fromEnv } overload only when the first
environment must configure the application before module registration.
Once the factory returns a configuration, that host instance caches it. If the factory itself throws, no configuration is cached and a later call may retry. Separate host instances do not share configuration or generations.
Common Patterns
Early Hints are unsupported
The Workers Response API does not provide a request-handler write primitive for an informational response before the final response, so context.response.earlyHints is absent. Check for capability presence before use. Cloudflare deployment/cache features that may generate Early Hints are host configuration and are not exposed as a Fluo response writer.
Streaming multipart consumption
Set multipart: { strategy: 'stream' } at application bootstrap to receive multipart data incrementally. For
multipart routes, RequestContext.request.body is an AsyncIterableIterator<MultipartPart>: field parts expose
kind: 'field', name, value, and headers; file parts expose kind: 'file', name, filename,
contentType, headers, and a single-consumer ReadableStream<Uint8Array> at stream. Finish or cancel each file
stream before requesting the next part.
Runtime route dispatch owns an iterator created for a route and automatically calls return() after the handler
finishes, cancelling and releasing an active source. Standalone parseMultipartStream(...) consumers own that
responsibility: consume the iterator to completion or call return() when ending early.
Byte Ranges and Cache Validation
Workers preserves the shared @fluojs/http single-byte-range and If-Range contract through fetch dispatch. After conditional-request evaluation selects cache validators, a valid Range: bytes= request yields the portable 206 identity-byte response; If-Range reuses those selected validators, while malformed or multi-range fields retain the full response and an unsatisfiable range yields bodyless 416. HEAD mirrors GET metadata without consuming a stream.
Working with WebSocketPairs
The adapter supports Cloudflare's native WebSocketPair for real-time communication via the @fluojs/websockets/cloudflare-workers binding. Upgrade handling is opt-in through that binding, and createWebSocketPair can be injected for non-hosted runtime tests. Configure the binding before listen() starts the Worker dispatch boundary; once listen() has run, the binding identity is frozen for that adapter instance. Replacing or clearing it is rejected even after close(), so upgrade ownership cannot change underneath an isolate that has already crossed the public listen boundary.
import { Module } from '@fluojs/core';
import { WebSocketGateway } from '@fluojs/websockets';
import { CloudflareWorkersWebSocketModule } from '@fluojs/websockets/cloudflare-workers';
@WebSocketGateway({ path: '/ws' })
export class EdgeGateway {}
@Module({
imports: [CloudflareWorkersWebSocketModule.forRoot()],
providers: [EdgeGateway],
})
export class RealtimeModule {}Import RealtimeModule into the application module graph before bootstrap. During application bootstrap, CloudflareWorkersWebSocketModule discovers the gateway and installs the Worker adapter binding through the version-1 realtime capability before app.listen() freezes it. Direct binding configuration is removed; do not add or replace the binding after the listen boundary.
Edge-Native Middleware
Standard fluo middleware (CORS, Global Prefix, etc.) is fully supported through
FluoFactory.create(...) and optimized for the Cloudflare environment.
CloudflareWorkerHttpApplicationAdapter.create(...) accepts adapter-owned parsing
and websocket-pair options; pass routing and middleware options to
FluoFactory.create(...) directly, or to the fixed-module host options when the
host-owned lazy capability is required.
const worker = CloudflareWorkerApplicationHost.create(AppModule, {
globalPrefix: 'api/v1',
cors: { origin: '*' },
});Behavior Notes
- The public concrete
CloudflareWorkerHttpApplicationAdapter.fetch(request, env, executionContext)contract requires the WorkerexecutionContext; direct callers must pass the real thirdctxargument so every HTTP, SSE, and WebSocket ingress registers active work withexecutionContext.waitUntil(...). Migration: replace direct two-argument adapter calls withadapter.fetch(request, env, ctx). fetch()registers active work withexecutionContext.waitUntil(...)afterlisten()or the lazy host binds the dispatcher; upgraded server WebSockets keep that lifecycle and the close drain open until their terminalcloseevent, while SSE (text/event-stream) responses keep them open until the body finishes or is canceled. Synchronous SSE reader or tracked-stream setup failures release the lifecycle before propagating. Before that lifecycle boundary, upgrade requests and HTTP dispatch do not reach application handlers.- Adapter options such as
maxBodySizeare validated byCloudflareWorkerHttpApplicationAdapter.create(...); application options such asglobalPrefix,cors,middleware, andsecurityHeadersbelong toFluoFactory.create(...)or the fixed-module host configuration. - WebSocket upgrades are owned by the same listen boundary as HTTP dispatch; upgrade requests before
listen()do not reach the configured binding, and attempts to replace or clear a defined binding after the adapter has ever listened fail fast instead of mutating Worker upgrade ownership. Create a new adapter when a host needs a different websocket binding. close()returns JSON503responses for new HTTP and WebSocket upgrade requests during and after shutdown and times out after 10 seconds if active requests never settle. Callinglisten()while that close drain is still active rejects with the Cloudflare Workers adapter shutdown-draining error. Lazy hosts do not permanently cache that timeout once the adapter's underlying drain later finishes.- The Worker
fetch(...)dispatch path preserves body-bearing RFCQUERYroutes and uppercase extension methods such asPURGE; their method token and parsed body reach the registered route through the same fetch dispatch seam. - Multipart requests do not preserve
rawBody. - The Worker
envobject is attached to eachFrameworkRequestasrequest.cloudflare.env, with the Worker execution context available asrequest.cloudflare.executionContext. DirectFluoFactory.create(...)completes module registration before its exportedfetch(...)handles traffic.CloudflareWorkerApplicationHost.create(AppModule, options)keeps its predeclared root module and options, so its fetch-timeenvattaches only during request dispatch. The{ fromEnv }overload is the opt-in alternative when the first explicit Worker environment must select the root module or final bootstrap options before module registration. Itsready(env)method requires that environment, caches the first environment's module and options once per host instance, and builds one application per application generation from that configuration. A successful close restarts the application without rerunning the factory or accepting later environments as bootstrap configuration. In either path, use request-boundrequest.cloudflare.envfor bindings that are intentionally per-request.
Lifecycle and Public Seam Notes
The root @fluojs/platform-cloudflare-workers export owns the Worker public seam for application code and first-party Worker websocket integrations. Worker-specific public types such as CloudflareWorkerExecutionContext, CloudflareWorkerRequestContext, CloudflareWorkerWebSocketBinding, CloudflareWorkerWebSocketPair, CloudflareWorkerWebSocketPairFactory, CloudflareWorkerWebSocketUpgradeHost, and CloudflareWorkerWebSocketUpgradeResult are exported from this package instead of asking consumers to import @fluojs/http/internal or @fluojs/runtime/internal* subpaths.
The listen, shutdown, SSE drain, and websocket binding rules above are public lifecycle behavior. Changes to those public seam types or lifecycle semantics are release-governed for @fluojs/platform-cloudflare-workers; user-impacting updates must be tracked with Changesets alongside the implementation, docs, and tests.
{
"closeOwnership": {
"inFetchManagement": "wait-until",
"outOfBand": "await",
"restart": "restartable"
},
"realtimeCapability": {
"bindingInstallationVersion": 1,
"contract": "raw-websocket-expansion",
"kind": "fetch-style",
"mode": "request-upgrade",
"support": "supported",
"version": 1
}
}Conformance Coverage
packages/platform-cloudflare-workers/src/adapter.test.ts and packages/platform-cloudflare-workers/src/adapter-lifecycle.test.ts are the package-local regression targets for the documented Worker contract. They cover shared Web dispatch delegation, Worker env request attachment, executionContext.waitUntil(...) SSE (text/event-stream) body tracking, body-cancellation and synchronous setup-failure drains, websocket upgrade binding, upgraded server-socket close tracking, pre-listen HTTP and websocket lifecycle guards, websocket binding freeze after the listen boundary, fixed-module and env-aware lazy host reuse, explicit env-aware readiness, first-environment configuration retention across successful host restarts, timeout recovery, shutdown gating, drain-time listen() rejection, JSON 503 responses while closing and after close for both HTTP and websocket upgrades, reliable fake-timer cleanup, public seam source imports, the structured realtime capability contract, and the bounded 10-second close timeout.
The shared edge portability suite in packages/testing/src/portability/web-runtime-adapter-portability.test.ts exercises Cloudflare Workers beside Bun and Deno for conditional requests, single-byte ranges and If-Range, body-bearing QUERY and PURGE fetch dispatch, malformed cookie preservation, query decoding, JSON/text raw-body capture, multipart raw-body exclusion, and SSE framing. The package test parses the structured realtime capability contract in both README locales and compares its machine-consumed values with the adapter capability.
Public API Overview
CloudflareWorkerHttpApplicationAdapter.create(options): Creates the concrete Worker HTTP adapter.CloudflareWorkerApplicationHost.create(AppModule, options): Creates a lazy fixed-module Worker host.CloudflareWorkerApplicationHost.create({ fromEnv }): Creates an env-aware lazy Worker host from its first explicit Worker environment.CloudflareWorkerApplicationGeneration<Env>: The hostready(...)result withapp, concreteadapter, extractablefetch, andclose(...).- Options and types:
CloudflareWorkerAdapterOptions,CloudflareWorkerApplicationHostOptions,CloudflareWorkerHostConfiguration,CloudflareWorkerHostFactory<Env>,CloudflareWorkerExecutionContext,CloudflareWorkerRequestContext,CloudflareWorkerWebSocketBinding,CloudflareWorkerWebSocket,CloudflareWorkerWebSocketMessage,CloudflareWorkerWebSocketPair,CloudflareWorkerWebSocketPairFactory,CloudflareWorkerWebSocketUpgradeHost, andCloudflareWorkerWebSocketUpgradeResult.
Related Packages
@fluojs/runtime: Core framework runtime.@fluojs/websockets: Includes specific subpath@fluojs/websockets/cloudflare-workers.@fluojs/http: Shared HTTP decorators.
Example Sources
packages/platform-cloudflare-workers/src/adapter.test.tspackages/websockets/src/cloudflare-workers/cloudflare-workers.test.ts
Bounded Body Parsing
CloudflareWorkerAdapterOptions inherits the runtime Web factory's HTTP-owned
bodyParser policy. CloudflareWorkerHttpApplicationAdapter.create(...) and
host configuration accept 'text', 'default', or a custom callback. Omission
retains MIME-based parsing and existing limits.
import { CloudflareWorkerHttpApplicationAdapter } from '@fluojs/platform-cloudflare-workers';
const adapter = CloudflareWorkerHttpApplicationAdapter.create({
bodyParser: 'text', maxBodySize: 1_048_576, rawBody: true,
});Headers and native Request identity are preserved. The runtime clone path keeps
the original body readable. Multipart remains independent. Parsing happens
before HTTP middleware/guards, so text alone does not guarantee auth order.
See the HTTP contract and matrix
for callback delegation, errors, empty bodies, and cancellation cooperation.
src/body-parser.test.ts runs the actual adapter with the shared wire fixture
and observes its waitUntil lifecycle. This is local Web conformance, not a
claim of a deployed Cloudflare isolate test.
