dioxus-cloudflare 0.2.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    │
│           │ ◀── JSON ──  │   ↓ http→worker resp │
└──────────┘               │   ↓ clear_context()  │
                           └─────────────────────┘
```

### 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`
   - Clears thread-local context
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::env()` | Full Worker `Env` — Durable Objects, Queues, etc. |
| `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)]` |
| `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
}
```

### 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()
├── 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()`)
- 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`
- 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/*"]`

### Not yet implemented

- Streaming response support (SSE)
- 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 (Medium)


Cloudflare Workers support `ReadableStream` for streaming responses. The current `handler.rs` collects the entire response body into `Vec<u8>` before sending. Streaming would require piping `axum::body::Body` frames into a `web_sys::ReadableStream`. Dioxus has transport-agnostic streaming architecture in `dioxus-server`, making this more tractable than full SSR.

**Recommendation:** Streaming server function responses first (simpler, more immediate value), full page SSR later.

## Roadmap


### Near-term

- **Durable Objects** — typed wrapper for `cf::env().durable_object()` with ergonomic state access
- **Queues** — producer API via `cf::env().queue()` for background job dispatch from server functions

### Medium-term

- **Streaming responses** — pipe `axum::body::Body``ReadableStream` for SSE and large payloads
- **Middleware hooks** — pre/post-dispatch hooks for auth checks, CORS, rate limiting, logging
- **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

## Dependencies


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