dioxus-cloudflare 0.7.0

Bridge between Dioxus server functions and Cloudflare Workers
Documentation
# dioxus-cloudflare


**The missing bridge between Dioxus server functions and Cloudflare Workers.**

[![crates.io](https://img.shields.io/crates/v/dioxus-cloudflare.svg)](https://crates.io/crates/dioxus-cloudflare)
[![docs.rs](https://docs.rs/dioxus-cloudflare/badge.svg)](https://docs.rs/dioxus-cloudflare)
[![license](https://img.shields.io/crates/l/dioxus-cloudflare.svg)](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


| 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`:

```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.

### 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)

### 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

- **Hydration** — client reuses server-rendered DOM instead of re-rendering (requires serialized state)
- **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


```toml
dioxus = { version = "=0.7.3", features = ["fullstack"] }
worker = { version = "0.4", features = ["http"] }
axum = { version = "0.8", default-features = false }
http = "1"
```