dioxus-cloudflare 0.7.11

Bridge between Dioxus server functions and Cloudflare Workers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
<div align="center">

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

</div>

---

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

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

### Session Middleware


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):**

```rust
Handler::new()
    .session(SessionConfig::kv("SESSIONS"))
    .handle(req, env)
    .await
```

**D1-backed sessions:**

```rust
Handler::new()
    .session(SessionConfig::d1("DB", "sessions"))
    .handle(req, env)
    .await
```

D1 requires a table with this schema:

```sql
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    data TEXT NOT NULL,
    expires_at INTEGER NOT NULL
);
```

**Reading and writing session data:**

```rust
#[server]

pub async fn login(user: String) -> Result<(), ServerFnError> {
    let session = cf::session().await?;
    session.set("user_id", &user)?;
    Ok(())
}

#[server]

pub async fn profile() -> Result<String, ServerFnError> {
    let session = cf::session().await?;
    let user: Option<String> = session.get("user_id")?;
    Ok(user.unwrap_or_else(|| "not logged in".into()))
}

#[server]

pub async fn logout() -> Result<(), ServerFnError> {
    let session = cf::session().await?;
    session.destroy();
    Ok(())
}
```

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

```rust
SessionConfig::kv("SESSIONS")
    .cookie_name("my_session")   // default: "__session"
    .max_age(60 * 60 * 24 * 7)  // 7 days (default: 86400 = 24h)
```

**wrangler.toml — add the KV namespace:**

```toml
[[kv_namespaces]]
binding = "SESSIONS"
id = "your-kv-namespace-id"
```

### Secrets and Environment Variables


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

```rust
#[server]

pub async fn verify_token(token: String) -> Result<bool, ServerFnError> {
    let expected = cf::secret("API_TOKEN")?.to_string();
    Ok(token == expected)
}
```

**Variables** are set in the `[vars]` section of `wrangler.toml` — plaintext, visible in source:

```rust
#[server]

pub async fn get_environment() -> Result<String, ServerFnError> {
    Ok(cf::var("ENVIRONMENT")?.to_string())
}
```

```toml
# wrangler.toml

[vars]
ENVIRONMENT = "production"
```

### Workers AI


Run AI inference from server functions using Cloudflare's built-in Workers AI models.

```rust
use serde::{Deserialize, Serialize};

#[derive(Serialize)]

struct AiInput { messages: Vec<AiMessage> }
#[derive(Serialize)]

struct AiMessage { role: String, content: String }
#[derive(Deserialize)]

struct AiOutput { response: Option<String> }

#[server]

pub async fn generate(prompt: String) -> Result<String, ServerFnError> {
    use dioxus_cloudflare::prelude::*;

    let ai = cf::ai("AI")?;
    let resp: AiOutput = ai.run("@cf/meta/llama-3.1-8b-instruct", AiInput {
        messages: vec![AiMessage { role: "user".into(), content: prompt }],
    }).await.cf()?;

    Ok(resp.response.unwrap_or_default())
}
```

```toml
# wrangler.toml

[ai]
binding = "AI"
```

Any model listed in the [Workers AI catalog](https://developers.cloudflare.com/workers-ai/models/) 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`.

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

### WebSocket Support


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

```rust
Handler::new()
    .websocket("/ws", |req| async move {
        let ns = cf::durable_object("WS_DO")?;
        let room = req.path().strip_prefix("/ws/").unwrap_or("default");
        let id = ns.id_from_name(room).cf()?;
        let stub = id.get_stub().cf()?;
        Ok(stub.fetch_with_request(req).await.cf()?)
    })
    .handle(req, env)
    .await
```

**Durable Object — accept the socket and handle messages:**

```rust
use worker::*;
use dioxus_cloudflare::prelude::*;

#[durable_object]

pub struct EchoDo {
    state: State,
    env: Env,
}

impl DurableObject for EchoDo {
    fn new(state: State, env: Env) -> Self { Self { state, env } }

    async fn fetch(&self, _req: Request) -> Result<Response> {
        let (server, resp) = cf::websocket_upgrade()?;
        self.state.accept_web_socket(&server);
        Ok(resp)
    }

    async fn websocket_message(&self, ws: WebSocket, message: WebSocketIncomingMessage) -> Result<()> {
        match message {
            WebSocketIncomingMessage::String(text) => ws.send_with_str(&format!("echo: {text}"))?,
            WebSocketIncomingMessage::Binary(bytes) => ws.send_with_bytes(&bytes)?,
        }
        Ok(())
    }

    async fn websocket_close(&self, _ws: WebSocket, _code: usize, _reason: String, _was_clean: bool) -> Result<()> {
        Ok(())
    }
}
```

**wrangler.toml — bind the DO and route WebSocket paths:**

```toml
[durable_objects]
bindings = [
  { name = "WS_DO", class_name = "EchoDo" }
]

[[migrations]]
tag = "v1"
new_sqlite_classes = ["EchoDo"]

[assets]
run_worker_first = ["/api/*", "/ws/*"]
```

### 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(), 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()
```

## 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()`, `cf::ai()`)
- **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`
- **WebSocket support**`Handler::websocket()` routes upgrade requests to Durable Objects; `cf::websocket_upgrade()` creates the `WebSocketPair` + 101 response
- **Session middleware**`Handler::session(SessionConfig::kv("SESSIONS"))` enables `cf::session()` in server functions; KV or D1 backend with automatic cookie management

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

- **Service bindings** — call other Workers from server functions via `cf::env().service()`
- **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()` / `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.7", features = ["http"] }
axum = { version = "0.8", default-features = false }
http = "1"
```

## License


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](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 |