# 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.
### 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
### Not yet implemented
- SSR rendering inside Workers
## SSR / Streaming Feasibility
### SSR (Hard)
`dioxus-server`'s SSR pipeline uses `LocalPoolHandle` (a tokio thread pool) for rendering. Cloudflare Workers run single-threaded WASM with no native thread support. The `dioxus-ssr` crate itself compiles to wasm, but the integration layer in `dioxus-server` that drives it does not. Adapting this requires a single-threaded executor replacement — a significant refactor.
### Streaming (Working)
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
- **SSR inside Workers** — requires single-threaded executor adaptation for `dioxus-server`'s render pipeline
- **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`) |
## Dependencies
```toml
dioxus = { version = "=0.7.3", features = ["fullstack"] }
worker = { version = "0.4", features = ["http"] }
axum = { version = "0.8", default-features = false }
http = "1"
```