dioxus-cloudflare
The missing bridge between Dioxus server functions and Cloudflare Workers.
What It Does
Write a #[server] function once. It runs on Cloudflare Workers. The client calls it like a normal async function. No manual routing, no manual serialization, no duplicated endpoints.
// shared crate — server function
use *;
use *;
pub async
// client component — just call it
let user = get_user.await;
Architecture
Client WASM Cloudflare Worker
┌──────────┐ fetch() ┌─────────────────────┐
│ #[server] │ ───────────▶ │ handle(req, env) │
│ generates │ │ ↓ set_context() │
│ POST to │ │ ↓ worker→http req │
│ /api/... │ │ ↓ Axum dispatch │
│ │ ◀─ stream ─ │ ↓ http→worker resp │
└──────────┘ └─────────────────────┘
Request Flow
- Client calls
get_user(id)— Dioxus serializes args, sends POST to/api/get_user - Worker
#[event(fetch)]receives the request dioxus_cloudflare::handle(req, env)is called:- Stores
Envin thread-local (cf::env()becomes available) - Stores raw
Requestin thread-local (cf::req()becomes available) - Converts
worker::Request→http::Request - Dispatches through the Dioxus Axum router (
axum_corefeature) - Converts
http::Response→worker::Response(streaming viaReadableStream)
- Stores
- Worker returns the response
Why Thread-Local Works
Cloudflare Workers run one request per isolate at a time (single-threaded WASM). There is no concurrent access to thread-locals within a single Worker invocation.
The Crate Provides
| Export | What It Does |
|---|---|
cf::d1(name) |
D1 database — env + binding + error conversion in one call |
cf::kv(name) |
Workers KV namespace |
cf::r2(name) |
R2 bucket |
cf::durable_object(name) |
Durable Object namespace |
cf::queue(name) |
Queue producer (requires queue feature) |
cf::env() |
Full Worker Env — for bindings without a shorthand |
cf::req() |
Raw worker::Request — headers, IP |
cf::cookie(name) |
Read a named cookie from the request |
cf::cookies() |
Read all cookies from the request |
cf::set_cookie() |
Set an HttpOnly/Secure auth cookie (secure defaults) |
cf::set_cookie_with() |
Set a cookie with custom options (builder pattern) |
cf::clear_cookie() |
Clear a cookie (logout) |
handle(req, env) |
Main entry point — wire this into #[event(fetch)] |
Handler |
Builder with before/after middleware hooks |
CfError |
Newtype for worker::Error → ServerFnError conversion |
CfResultExt |
.cf() method on Result<T, worker::Error> and Result<T, KvError> |
Prerequisites
This crate requires a patched version of dioxus-server that adds wasm32 target support. Add the following to your workspace Cargo.toml:
[]
= { = "https://github.com/JaffeSystems/dioxus-server-cf.git" }
This is necessary because upstream dioxus-server 0.7.3 does not compile for wasm32-unknown-unknown. The patch applies minimal cfg-gating to make it compatible with Cloudflare Workers.
Usage
Worker Entry Point
use *;
use *;
// Import server functions so they register with inventory
use *;
extern "C"
async
Middleware Hooks
Use [Handler] for before/after middleware without touching bridge internals.
CORS headers on all responses:
use *;
use Handler;
async
Auth check (short-circuit unauthorized requests):
new
.before
.handle
.await
Before hooks run after context is set (cf::env(), cf::d1(), etc. work). Return Ok(None) to continue, Ok(Some(resp)) to short-circuit. After hooks run on all responses (including short-circuited ones) and can modify headers.
SSR (Server-Side Rendering)
Render Dioxus components to HTML at the edge. Requires the ssr feature.
When the Axum router returns 404 and the request accepts text/html, the handler renders your app component and returns the HTML. Non-HTML requests (JS, CSS, WASM, JSON) pass through normally.
Minimal SSR (default HTML shell, no client JS):
new
.with_ssr
.handle
.await
SSR with custom index.html (SPA takeover after first paint):
new
.with_ssr
.with_index_html?
.handle
.await
The custom index.html must contain an element with id="main" — rendered component output is inserted at that point.
Suspense is supported: wait_for_suspense() resolves server futures during SSR, so components that call #[server] functions via use_server_future will have their data ready in the initial HTML.
SSR with Hydration
SSR always renders with hydration markers (data-node-hydration attributes) and injects serialized hydration data. When the client WASM is built with hydrate(true), it reuses the server-rendered DOM instead of re-rendering — providing instant first paint with no flash.
Worker (server):
new
.with_ssr
.with_index_html?
.handle
.await
Client WASM (must render the same component):
The client must enable the fullstack feature on dioxus (which activates dioxus-web/hydrate):
# Cargo.toml
[]
= { = "=0.7.3", = ["web", "fullstack"] }
Important: Do not use ? on use_server_future in hydrated components. The ? operator suspends the component if the resource isn't immediately ready, which creates a VirtualDom/DOM tree mismatch and crashes the hydration walker. Instead, match on the Result:
Build order:
dx build --release— builds client WASM +index.htmlcargo build --release --target wasm32-unknown-unknown -p your-worker— worker includesindex.htmlviainclude_str!
Server Function (Shared Crate)
use *;
use *;
pub async
Client Component
use *;
use create_order;
Crate Structure
src/
├── lib.rs # Public API: cf module, handle(), re-exports
├── bindings.rs # Typed binding shorthands: d1(), kv(), r2(), durable_object(), queue()
├── context.rs # Thread-local Env + Request storage
├── handler.rs # handle() — Worker↔Axum request/response bridge
├── cookie.rs # Cookie read/write helpers + CookieBuilder
├── error.rs # CfError newtype + CfResultExt trait
└── prelude.rs # Convenience re-exports
Implementation Status
Working end-to-end
-
Thread-local context (
cf::env(),cf::req()) -
Typed binding shorthands (
cf::d1(),cf::kv(),cf::r2(),cf::durable_object(),cf::queue()) -
Durable Objects —
cf::durable_object()shorthand for namespace access -
Queues —
cf::queue()shorthand for producer access (requiresqueuefeature) -
Request headers — read via
cf::req()inside server functions -
Cookie reading (
cf::cookie(),cf::cookies()) -
Cookie writing (
cf::set_cookie(),cf::clear_cookie()) — queue-based, applied to response automatically -
Configurable cookie builder (
cf::set_cookie_with()) — customSameSite,Domain,Path,Max-Age, etc. -
Error bridge (
CfError,.cf()) — covers bothworker::ErrorandKvError -
Streaming responses —
TextStream,ByteStream, and other streaming payloads viaReadableStream -
Request/response conversion (
worker::Request↔http::Request) -
dispatch()— Axum router withServerFunction::collect()+.oneshot() -
__wasm_call_ctors+inventoryinitialization -
D1 — read/write queries via
cf::d1("DB") -
Workers KV — put/get/delete via
cf::kv("KV") -
R2 — put/get/delete via
cf::r2("BUCKET") -
Static assets via wrangler
[assets]+run_worker_first = ["/api/*"] -
Middleware hooks —
Handlerbuilder with before/after hooks for auth, CORS, custom headers -
SSR inside Workers — render Dioxus components to HTML at the edge (requires
ssrfeature) -
Hydration — client reuses server-rendered DOM instead of re-rendering (SSR always emits hydration markers)
Streaming
Response bodies are streamed via ReadableStream — handler.rs wraps axum::body::Body as a TryStream and pipes it through ResponseBuilder::from_stream(). Server functions returning TextStream, ByteStream, JsonStream, etc. stream chunks directly to the client without buffering the full response into memory.
Roadmap
Near-term
- Template project /
cargo generate— scaffolding for new dioxus-cloudflare projects with wrangler config, shared/web/worker crates, and build scripts - Remove
dioxus-serverpatch requirement — upstream the wasm32cfg-gating to Dioxus core so users don't need[patch.crates-io]
Ideas / Exploration
- Streaming SSR — send HTML chunks as suspense resolves via ReadableStream
- Session middleware — built-in session management backed by KV or D1 with cookie-based session IDs
- WebSocket support — Durable Object + WebSocket upgrade for real-time Dioxus apps
- Service bindings — call other Workers from server functions via
cf::env().service() - AI bindings —
cf::env().ai()for Workers AI inference from server functions - Secrets —
cf::env().secret()for accessing encrypted environment variables - Wrangler plugin — automate the build pipeline (cargo build → wasm-bindgen → shim generation) without manual wrangler.toml
[build]commands
Optional Features
| Feature | Enables |
|---|---|
queue |
cf::queue() shorthand (activates worker/queue) |
ssr |
Server-side rendering via Handler::with_ssr() (adds dioxus-ssr, dioxus-history) |
Dependencies
= { = "=0.7.3", = ["fullstack"] }
= { = "0.4", = ["http"] }
= { = "0.8", = false }
= "1"