# dioxus-cloudflare
**The missing bridge between Dioxus server functions and Cloudflare Workers.**
[](https://crates.io/crates/dioxus-cloudflare)
[](https://docs.rs/dioxus-cloudflare)
[](LICENSE-MIT)
---
## 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.
```rust
// shared crate — server function
use dioxus::prelude::*;
use dioxus_cloudflare::prelude::*;
#[server]
pub async fn get_user(id: String) -> Result<User, ServerFnError> {
let db = cf::d1("DB")?;
db.prepare("SELECT * FROM users WHERE id = ?")
.bind(&[id.into()])?
.first::<User>(None)
.await
.cf()?
.ok_or_else(|| ServerFnError::new("Not found"))
}
```
```rust
// client component — just call it
let user = get_user("abc".into()).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
1. Client calls `get_user(id)` — Dioxus serializes args, sends POST to `/api/get_user`
2. Worker `#[event(fetch)]` receives the request
3. `dioxus_cloudflare::handle(req, env)` is called:
- Stores `Env` in thread-local (`cf::env()` becomes available)
- Stores raw `Request` in thread-local (`cf::req()` becomes available)
- Converts `worker::Request` → `http::Request`
- Dispatches through the Dioxus Axum router (`axum_core` feature)
- Converts `http::Response` → `worker::Response` (streaming via `ReadableStream`)
4. 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
| `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`:
```toml
[patch.crates-io]
dioxus-server = { git = "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
```rust
use worker::*;
use dioxus_cloudflare::prelude::*;
// Import server functions so they register with inventory
use shared::server_fns::*;
extern "C" { fn __wasm_call_ctors(); }
#[event(fetch)]
async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
// Required: initialize inventory for #[server] function registration
// SAFETY: Called once per cold start. inventory crate needs this in WASM.
unsafe { __wasm_call_ctors(); }
dioxus_cloudflare::handle(req, env).await
}
```
### Middleware Hooks
Use [`Handler`] for before/after middleware without touching bridge internals.
**CORS headers on all responses:**
```rust
use worker::*;
use dioxus_cloudflare::Handler;
#[event(fetch)]
async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
unsafe { __wasm_call_ctors(); }
Handler::new()
.after(|resp| {
resp.headers_mut().set("Access-Control-Allow-Origin", "*")?;
Ok(())
})
.handle(req, env)
.await
}
```
**Auth check (short-circuit unauthorized requests):**
```rust
Handler::new()
.before(|req| {
if req.headers().get("Authorization")?.is_none() {
return Ok(Some(Response::error("Unauthorized", 401)?));
}
Ok(None) // continue to server functions
})
.handle(req, env)
.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):**
```rust
Handler::new()
.with_ssr(App)
.handle(req, env)
.await
```
**SSR with custom index.html (SPA takeover after first paint):**
```rust
Handler::new()
.with_ssr(App)
.with_index_html(include_str!("path/to/index.html"))?
.handle(req, env)
.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):**
```rust
Handler::new()
.with_ssr(App)
.with_index_html(include_str!("path/to/index.html"))?
.handle(req, env)
.await
```
**Client WASM (must render the same component):**
The client must enable the `fullstack` feature on `dioxus` (which activates `dioxus-web/hydrate`):
```toml
# Cargo.toml
[dependencies]
dioxus = { version = "=0.7.3", features = ["web", "fullstack"] }
```
```rust
fn main() {
dioxus::launch(App);
}
```
**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`:
```rust
#[component]
fn App() -> Element {
let data_text = match use_server_future(get_data) {
Ok(resource) => match &*resource.read() {
Some(Ok(s)) => s.clone(),
Some(Err(e)) => format!("Error: {e}"),
None => "Loading...".into(),
},
Err(_) => "Loading...".into(),
};
rsx! { p { "{data_text}" } }
}
```
**Build order:**
1. `dx build --release` — builds client WASM + `index.html`
2. `cargo build --release --target wasm32-unknown-unknown -p your-worker` — worker includes `index.html` via `include_str!`
### Streaming SSR
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.
```rust
Handler::new()
.with_streaming_ssr(App)
.with_index_html(include_str!("path/to/index.html"))?
.handle(req, env)
.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.
### Server Function (Shared Crate)
```rust
use dioxus::prelude::*;
use dioxus_cloudflare::prelude::*;
#[server]
pub async fn create_order(items: Vec<Item>) -> Result<Order, ServerFnError> {
let db = cf::d1("DB")?;
db.prepare("INSERT INTO orders (items, total) VALUES (?, ?)")
.bind(&[serde_json::to_string(&items)?.into(), total.into()])?
.run()
.await
.cf()?;
Ok(Order { items, total, status: "confirmed".into() })
}
```
### Client Component
```rust
use dioxus::prelude::*;
use shared::server_fns::create_order;
#[component]
fn OrderButton(items: Vec<Item>) -> Element {
let order = use_resource(move || {
let items = items.clone();
async move { create_order(items).await }
});
match &*order.read() {
Some(Ok(o)) => rsx! { p { "Order confirmed: {o.status}" } },
Some(Err(e)) => rsx! { p { "Error: {e}" } },
None => rsx! { p { "Placing 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 (requires `queue` feature)
- 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()`) — custom `SameSite`, `Domain`, `Path`, `Max-Age`, etc.
- Error bridge (`CfError`, `.cf()`) — covers both `worker::Error` and `KvError`
- **Streaming responses** — `TextStream`, `ByteStream`, and other streaming payloads via `ReadableStream`
- Request/response conversion (`worker::Request` ↔ `http::Request`)
- `dispatch()` — Axum router with `ServerFunction::collect()` + `.oneshot()`
- `__wasm_call_ctors` + `inventory` initialization
- **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** — `Handler` builder with before/after hooks for auth, CORS, custom headers
- **SSR inside Workers** — render Dioxus components to HTML at the edge (requires `ssr` feature)
- **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`
### 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-server` patch requirement** — upstream the wasm32 `cfg`-gating to Dioxus core so users don't need `[patch.crates-io]`
### Ideas / Exploration
- **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
| `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`) |
## Dependencies
```toml
dioxus = { version = "=0.7.3", features = ["fullstack"] }
worker = { version = "0.4", features = ["http"] }
axum = { version = "0.8", default-features = false }
http = "1"
```