> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Workers

> Deploy a full-stack Vertz app to Cloudflare Workers with SSR, API routing, and ISR caching

`@vertz/cloudflare` provides `createHandler` — a single function that wires up your API, SSR, security headers, and optional ISR caching into a Cloudflare Worker module.

## Quick start

```ts theme={null}
// src/worker.ts
import { createHandler } from '@vertz/cloudflare';
import { createDb } from '@vertz/db';
import { createServer, type ServerConfig } from '@vertz/server';
import * as app from '../dist/server/app';
import { todos } from './api/entities/todos/todos.entity';
import { todosModel } from './api/schema';

interface Env {
  DB: D1Database;
}

export default createHandler({
  app: (env) => {
    const typedEnv = env as Env;
    const db = createDb({
      models: { todos: todosModel },
      dialect: 'sqlite',
      d1: typedEnv.DB,
    });
    return createServer({ entities: [todos], db });
  },
  ssr: {
    module: app,
    clientScript: '/assets/entry-client.js',
    title: 'My App',
  },
});
```

That's it. The handler automatically:

* Routes `/api/*` requests to your server (entities, auth, services)
* Renders all other routes via SSR
* Adds security headers with per-request nonce-based CSP
* Detects `requestHandler` on `ServerInstance` for auth-aware routing

## Configuration

### `CloudflareHandlerConfig`

```ts theme={null}
createHandler({
  // Required — factory that creates your Vertz server.
  // Receives Worker env bindings. Called once, then cached.
  app: (env) => createServer({ ... }),

  // Required — SSR configuration for non-API routes.
  // Pass an SSR module config (zero-boilerplate) or a custom callback.
  ssr: { module: app },

  // Optional — API path prefix. Default: '/api'
  // Matches createServer's apiPrefix default, so you rarely need this.
  apiPrefix: '/api',

  // Optional — add security headers to all responses. Default: true
  // Includes CSP with per-request nonce, HSTS, X-Frame-Options, etc.
  securityHeaders: true,

  // Optional — ISR cache configuration (see below)
  cache: { ... },

  // Optional — image optimizer from @vertz/cloudflare/image
  imageOptimizer: imageOptimizer({ allowedDomains: ['cdn.example.com'] }),

  // Optional — middleware hook before SSR rendering.
  // Return a Response to short-circuit (e.g., redirect to /login).
  beforeRender: async (request, env) => {
    if (needsAuth(request)) {
      return Response.redirect('/login');
    }
  },
});
```

### SSR module config

The zero-boilerplate form passes your app module directly:

```ts theme={null}
ssr: {
  // Your app module — must export App, and optionally theme, styles, getInjectedCSS
  module: app,

  // Client-side entry script path. Default: '/assets/entry-client.js'
  clientScript: '/assets/entry-client.js',

  // HTML document title. Default: 'Vertz App'
  title: 'My App',

  // SSR query timeout in ms. Default: 5000 (generous for D1 cold starts)
  ssrTimeout: 5000,
}
```

For full control, pass a custom callback instead:

```ts theme={null}
ssr: async (request) => {
  const html = await renderMyPage(request);
  return new Response(html, {
    headers: { 'Content-Type': 'text/html' },
  });
};
```

## Route splitting

The handler splits requests by URL path:

| Route           | Handler                                   |
| --------------- | ----------------------------------------- |
| `/api/*`        | Server handler (entities, auth, services) |
| `/_vertz/image` | Image optimizer (if configured)           |
| Everything else | SSR                                       |

Static assets (JS, CSS, images) are served by Cloudflare's `[assets]` directive in `wrangler.toml` **before** the Worker runs — they never reach `createHandler`.

### Custom apiPrefix

If your server uses a non-default `apiPrefix`, match it in the handler:

```ts theme={null}
createHandler({
  app: (env) => createServer({ apiPrefix: '/v1' }),
  apiPrefix: '/v1',
  ssr: { module: app },
});
```

When `apiPrefix` is omitted from `createHandler`, it reads `app.apiPrefix` at
runtime — so if you set it on `createServer`, the handler picks it up automatically.

## Auth-aware routing

When your server uses auth (OAuth, sessions), `createServer` returns a `ServerInstance` with a `requestHandler` method that routes both auth and entity requests. The handler detects this automatically:

```ts theme={null}
// requestHandler is auto-detected — auth routes just work
createHandler({
  app: (env) =>
    createServer({
      entities: [todos],
      db,
      auth: { providers: [github()] },
    }),
  ssr: { module: app },
});
```

No manual wiring needed. `/api/auth/signin`, `/api/auth/callback`, etc. are all handled.

If your app is a plain `AppBuilder` without auth, the handler falls back to `app.handler` automatically.

## Security headers

Security headers are enabled by default. Every response includes:

| Header                      | Value                                                                                                             |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `Content-Security-Policy`   | `default-src 'self'; script-src 'self' 'nonce-<random>'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;` |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains`                                                                             |
| `X-Content-Type-Options`    | `nosniff`                                                                                                         |
| `X-Frame-Options`           | `DENY`                                                                                                            |
| `X-XSS-Protection`          | `1; mode=block`                                                                                                   |
| `Referrer-Policy`           | `strict-origin-when-cross-origin`                                                                                 |

Each request gets a unique cryptographic nonce for CSP. Script tags in SSR output use this nonce.

To disable security headers (not recommended):

```ts theme={null}
createHandler({
  app: (env) => createServer({ ... }),
  ssr: { module: app },
  securityHeaders: false,
});
```

## ISR caching

ISR (Incremental Static Regeneration) caches SSR responses in Cloudflare KV. This gives you near-static performance with dynamic content.

### How it works

1. **First request** — SSR renders the page, stores the HTML in KV, returns the response (`X-Vertz-Cache: MISS`)
2. **Subsequent requests (within TTL)** — serves from KV instantly (`X-Vertz-Cache: HIT`)
3. **After TTL expires** — serves the stale page immediately, re-renders in the background via `ctx.waitUntil()` (`X-Vertz-Cache: STALE`)

### Setup

Add a KV namespace in `wrangler.toml`:

```toml theme={null}
[[kv_namespaces]]
binding = "PAGE_CACHE"
id = "your-kv-namespace-id"
```

Configure the cache:

```ts theme={null}
interface Env {
  DB: D1Database;
  PAGE_CACHE: KVNamespace;
}

export default createHandler({
  app: (env) => createServer({ ... }),
  ssr: { module: app },
  cache: {
    // Factory that returns the KV namespace from Worker env bindings
    kv: (env) => (env as Env).PAGE_CACHE,

    // Cache TTL in seconds. Default: 3600 (1 hour)
    ttl: 3600,

    // Serve stale while revalidating in background. Default: true
    // Set to false to force synchronous re-render on stale entries.
    staleWhileRevalidate: true,
  },
});
```

### Cache behavior

* Only SSR routes are cached — API requests (`/api/*`) are never cached
* Nonces are stripped before caching and re-injected on each request (each response gets a fresh CSP nonce)
* KV entries expire at 2x the TTL to allow stale-while-revalidate to work
* KV lookup failures are non-fatal — the handler falls through to SSR

## beforeRender middleware

Use `beforeRender` to run logic before SSR on non-API routes. Return a `Response` to short-circuit, or `undefined` to proceed with SSR:

```ts theme={null}
createHandler({
  app: (env) => createServer({ ... }),
  ssr: { module: app },
  beforeRender: async (request, env) => {
    const session = await getSession(request);
    if (!session && isProtectedRoute(request.url)) {
      return Response.redirect('/login', 302);
    }
    // Return undefined to proceed with normal SSR
  },
});
```

`beforeRender` receives the `Request` and the Worker `env` bindings. It runs after API routing but before SSR and ISR cache checks.

## Wrangler configuration

```toml theme={null}
# wrangler.toml
name = "my-app"
main = "src/worker.ts"
compatibility_date = "2025-01-01"

# Static assets served before the Worker runs
[assets]
directory = "./dist/client"

# D1 database binding
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "your-database-id"

# KV for ISR caching (optional)
[[kv_namespaces]]
binding = "PAGE_CACHE"
id = "your-kv-namespace-id"
```

<Note>
  The `[assets]` directive serves static files (JS bundles, CSS, images) directly from Cloudflare's
  edge — they never hit the Worker. Only dynamic requests (API calls, page navigations) reach
  `createHandler`.
</Note>

## Deploy

```bash theme={null}
# Build the app
vertz build

# Deploy to Cloudflare
vtzx wrangler deploy
```

## How it differs from static deployment

|              | Static site                 | Full-stack (Workers)        |
| ------------ | --------------------------- | --------------------------- |
| **Server**   | None — static files only    | Cloudflare Worker with SSR  |
| **Data**     | Build-time only             | Per-request (queries, auth) |
| **Package**  | Custom build script         | `@vertz/cloudflare`         |
| **Caching**  | Edge CDN (immutable assets) | ISR via KV (optional)       |
| **Use case** | Landing pages, marketing    | Apps with dynamic data      |

<CardGroup cols={2}>
  <Card title="Static Sites" icon="file" href="/guides/deploy/static-sites">
    Deploy static sites and landing pages
  </Card>

  <Card title="SSR" icon="server" href="/guides/ui/ssr">
    How Vertz SSR works under the hood
  </Card>
</CardGroup>
