dioxus-cloudflare
The missing bridge between Dioxus server functions and Cloudflare Workers.
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;
Client WASM Cloudflare Worker
┌──────────┐ fetch() ┌─────────────────────┐
│ #[server] │ ───────────▶ │ handle(req, env) │
│ generates │ │ ↓ set_context() │
│ POST to │ │ ↓ worker→http req │
│ /api/... │ │ ↓ Axum dispatch │
│ │ ◀─ stream ─ │ ↓ http→worker resp │
└──────────┘ └─────────────────────┘
- 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
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.
| 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::secret(name) |
Encrypted secret (wrangler secret put) |
cf::var(name) |
Plaintext environment variable ([vars] in wrangler.toml) |
cf::ai(name) |
Workers AI inference |
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) |
cf::session() |
Load session data (async); returns Session handle for sync get/set/remove |
SessionConfig |
Session backend configuration (KV or D1) — pass to Handler::session() |
handle(req, env) |
Main entry point — wire this into #[event(fetch)] |
Handler |
Builder with before/after middleware hooks + .session() + .websocket() routing |
cf::websocket_upgrade() |
Create a WebSocketPair + 101 response in one call (for Durable Objects) |
cf::websocket_pair() |
Create a raw WebSocketPair for custom handling |
CfError |
Newtype for worker::Error → ServerFnError conversion |
CfResultExt |
.cf() method on Result<T, worker::Error> and Result<T, KvError> |
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.
use *;
use *;
// Import server functions so they register with inventory
use *;
extern "C"
async
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.
Built-in session management backed by Workers KV or D1. Configure it on the Handler builder — cf::session() becomes available in all server functions.
KV-backed sessions (automatic expiry via KV TTL):
new
.session
.handle
.await
D1-backed sessions:
new
.session
.handle
.await
D1 requires a table with this schema:
(
id TEXT PRIMARY KEY,
data TEXT NOT NULL,
expires_at INTEGER NOT NULL
);
Reading and writing session data:
pub async
pub async
pub async
cf::session() is async (loads from KV/D1 on first call, cached after). Session methods (get, set, remove, destroy) are sync — they operate on the in-memory cache. Dirty data is flushed to the backend automatically before the response is sent.
Custom configuration:
kv
.cookie_name // default: "__session"
.max_age // 7 days (default: 86400 = 24h)
wrangler.toml — add the KV namespace:
[[]]
= "SESSIONS"
= "your-kv-namespace-id"
Access encrypted secrets and plaintext variables from inside server functions.
Secrets are set via wrangler secret put or the Cloudflare dashboard — encrypted at rest, never in wrangler.toml:
pub async
Variables are set in the [vars] section of wrangler.toml — plaintext, visible in source:
pub async
# wrangler.toml
[]
= "production"
Run AI inference from server functions using Cloudflare's built-in Workers AI models.
use ;
pub async
# wrangler.toml
[]
= "AI"
Any model listed in the Workers AI catalog can be used — text generation, embeddings, image generation, etc. Define typed input/output structs matching the model's API — serde_json::Value does not work correctly through serde_wasm_bindgen.
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 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!
Send the initial HTML immediately with suspense fallbacks as placeholders, then stream resolved content out-of-order via ReadableStream as each suspense boundary completes. Fast data renders instantly; slow data streams in later. Requires the ssr feature.
new
.with_streaming_ssr
.with_index_html?
.handle
.await
If no suspense boundaries are pending after the initial render, streaming SSR automatically falls back to a single-shot response with no overhead — you can always use with_streaming_ssr without penalty.
The client-side JavaScript (window.dx_hydrate) swaps suspense placeholders with resolved content as chunks arrive. This is the same mechanism used by upstream Dioxus streaming SSR.
Real-time WebSocket connections via Durable Objects. The worker upgrades the request and forwards it to a DO, which creates the WebSocketPair and handles messages.
Worker entry point — route WebSocket upgrades to a Durable Object:
new
.websocket
.handle
.await
Durable Object — accept the socket and handle messages:
use *;
use *;
wrangler.toml — bind the DO and route WebSocket paths:
[]
= [
{ name = "WS_DO", class_name = "EchoDo" }
]
[[]]
= "v1"
= ["EchoDo"]
[]
= ["/api/*", "/ws/*"]
use *;
use *;
pub async
use *;
use create_order;
src/
├── lib.rs # Public API: cf module, handle(), re-exports
├── bindings.rs # Typed binding shorthands: d1(), kv(), r2(), durable_object(), queue(), ai()
├── context.rs # Thread-local Env + Request storage
├── handler.rs # handle() — Worker↔Axum bridge, Handler builder, SSR fallback
├── cookie.rs # Cookie read/write helpers + CookieBuilder
├── error.rs # CfError newtype + CfResultExt trait
├── prelude.rs # Convenience re-exports
├── session.rs # Session middleware: KV/D1 backend, cookie-based session IDs
├── ssr.rs # SSR rendering: single-shot + streaming, hydration data extraction
├── streaming.rs # Out-of-order streaming data structures: MountPath, Mount, PendingSuspenseBoundary
└── websocket.rs # WebSocket helpers: websocket_upgrade(), websocket_pair()
- 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 SSR — send initial HTML with suspense fallbacks immediately, stream resolved content out-of-order via
ReadableStream - WebSocket support —
Handler::websocket()routes upgrade requests to Durable Objects;cf::websocket_upgrade()creates theWebSocketPair+ 101 response - Session middleware —
Handler::session(SessionConfig::kv("SESSIONS"))enablescf::session()in server functions; KV or D1 backend with automatic cookie management
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.
| Feature | Enables |
|---|---|
queue |
cf::queue() shorthand (activates worker/queue) |
ssr |
Server-side rendering via Handler::with_ssr() / with_streaming_ssr() (adds dioxus-ssr, dioxus-history, futures-channel, wasm-bindgen-futures) |
= { = "=0.7.3", = ["fullstack"] }
= { = "0.7", = ["http"] }
= { = "0.8", = false }
= "1"
Copyright (C) 2026-2027 Jaffe Systems
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
If you use this software in a network service (SaaS, web application, API, etc.), you must make the complete source code of your application available to its users under the AGPL-3.0. This includes any modifications and derivative works.
Commercial License: If you need to use this software in a proprietary or closed-source application without the AGPL-3.0 obligations, a commercial license is available. See COMMERCIAL-LICENSE.md for details.
| Use Case | License | Source Disclosure Required? |
|---|---|---|
| Open-source project | AGPL-3.0 (free) | Yes |
| Internal tools (not served to users) | AGPL-3.0 (free) | No |
| Proprietary SaaS / closed-source | Commercial (paid) | No |