---
title: "The web tier on Cloudflare Workers"
description: "Run a reduced Quire web tier on Cloudflare Workers."
image: "https://docs.quirelms.com/og.png"
---

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

# The web tier on Cloudflare Workers

<span id="the-web-tier-on-cloudflare-workers"></span>

The design is `docs/architecture/23-ops.md` section 6. Workers run a reduced
web tier. Feature parity on Workers is out of scope (PRD section 12): what
this target cannot do is refused when it starts, by name.

## Status in this release <!--quire:status-in-this-release-->

The configuration is in place (`apps/web/wrangler.jsonc`, the
`cloudflare-module` Nitro preset, the Hyperdrive bridge and the start-up
checks). A Worker needs S3-compatible storage for R2
(`QUIRE_STORAGE_DRIVER=s3`), a realtime driver across requests
(`QUIRE_REALTIME_DRIVER=durable_objects` or `centrifugo`), a shared cache
(`QUIRE_CACHE_DRIVER=postgres` or `valkey`) and an HTTP email provider.
Without them the Worker refuses to start and its log names each setting.
The Durable Objects driver is a client of the realtime Worker in
`apps/realtime-worker` (one Durable Object per channel for fan-out, presence
and history, one per person for disconnects); deploy it alongside, as below,
or use Centrifugo.

## The pieces <!--quire:the-pieces-->

| Piece | On Cloudflare |
| --- | --- |
| `web` | A Worker with `nodejs_compat` |
| Postgres | External, reached through Hyperdrive: `HYPERDRIVE` for the application role, `REPORT_HYPERDRIVE` for the report role on that same physical database. The web tier copies each connection string into `DATABASE_URL` and `QUIRE_REPORT_DATABASE_URL` when it starts |
| Files | R2, through its S3 API (`S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com`); the `FILES` binding attaches the bucket |
| Background jobs | pg-boss over Hyperdrive when the job must be enqueued with a write. With `QUIRE_QUEUE_DRIVER=cloudflare` on the companion worker, the light jobs (unordered notification and webhook deliveries) go through Cloudflare Queues instead, so Postgres is not polled for them. The companion worker runs both |
| Realtime | The realtime Worker, `apps/realtime-worker`, with Durable Objects |
| `worker`, `scheduler`, `collab`, `content`, ClamAV, Gotenberg, ffmpeg | A companion container host. A Worker cannot run them |
| Tracing | Workers observability, enabled in `wrangler.jsonc` |

`REPORT_HYPERDRIVE` supplies the report role for the physical database named
by `HYPERDRIVE`. This target does not serve tenants pinned to additional
physical databases, as described below.

## What this target cannot do <!--quire:what-this-target-cannot-do-->

Refused at start, with every problem listed at once:

- **No SMTP.** Use an HTTP provider in `QUIRE_EMAIL_PROVIDER_CONFIG`.
- **No local disk.** `QUIRE_STORAGE_DRIVER` must name object storage.
- **No in-process realtime or cache.** Workers share no memory between
  requests, so `QUIRE_REALTIME_DRIVER=inprocess` and
  `QUIRE_CACHE_DRIVER=memory` are refused.
- **No ClamAV, Gotenberg or ffmpeg in the Worker.** `CLAMAV_URL`,
  `GOTENBERG_URL` and `FFMPEG_PATH` set on the Worker are refused; set them on
  the companion worker instead.
- **No dedicated-database organisations.** A Worker's Hyperdrive bindings are
  fixed at deploy time, so an organisation with its own database gets a clear
  "unavailable here" page. Serve it from Compose or Vercel.

And one thing that is not refused but must be known: **prerendering and
incremental static regeneration do not work on Workers**, whatever the
framework documentation says. Every route renders per request.

On this target keep transactions short and never hold one across a network
call: Hyperdrive resets session state when a connection returns to the pool,
so the tenant context is set per transaction.

## Deploying <!--quire:deploying-->

1. Create the resources:
   ```sh
   bun run --bun wrangler hyperdrive create quire-app --connection-string="postgres://quire_app:...@db.example.com:5432/quire"
   bun run --bun wrangler hyperdrive create quire-report --connection-string="postgres://quire_report:...@db.example.com:5432/quire"
   bun run --bun wrangler r2 bucket create quire-files
   bun run --bun wrangler queues create quire-jobs
   ```
   Put the two Hyperdrive ids into `apps/web/wrangler.jsonc`.
2. Set the secrets, one at a time with `bun run --bun wrangler secret put <NAME>` from
   `apps/web`: `QUIRE_SECRET_KEY`, `QUIRE_MASTER_KEY`,
   `QUIRE_EMAIL_PROVIDER_CONFIG`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`,
   `QUIRE_COLLAB_SIGNING_KEY`, `QUIRE_REALTIME_WORKER_SECRET`. Plain settings
   (`QUIRE_APP_ORIGIN`, `QUIRE_CONTENT_ORIGIN`, `QUIRE_PLATFORM_DOMAINS`,
   `QUIRE_DATABASE_ID`, `S3_ENDPOINT`, `S3_BUCKET`, `QUIRE_COLLAB_URL`) go in
   `vars`.
3. Build and deploy from `apps/web`:
   ```sh
   NITRO_PRESET=cloudflare-module bun run build
   bun run --bun wrangler deploy
   ```
4. Deploy the realtime Worker, with the web tier's `QUIRE_REALTIME_WORKER_SECRET`
   and its token secret as `QUIRE_REALTIME_TOKEN_SECRET` (the web tier signs
   realtime tokens with its own `QUIRE_REALTIME_TOKEN_SECRET`, or with
   `QUIRE_SECRET_KEY` when that is unset, so use whichever it uses), and set
   `QUIRE_REALTIME_WORKER_URL` on the web tier to its address:
   ```sh
   cd apps/realtime-worker
   bun run --bun wrangler secret put QUIRE_REALTIME_WORKER_SECRET
   bun run --bun wrangler secret put QUIRE_REALTIME_TOKEN_SECRET
   bun run --bun wrangler deploy
   ```
5. For light jobs on Cloudflare Queues, create one queue per light queue and
   set these on the companion worker (an API token with Queues read and
   write):
   ```sh
   bun run --bun wrangler queues create quire-events-notifications
   bun run --bun wrangler queues create quire-events-notifications-dead
   bun run --bun wrangler queues create quire-events-webhooks
   bun run --bun wrangler queues create quire-events-webhooks-dead
   ```
   `QUIRE_QUEUE_DRIVER=cloudflare`, `CLOUDFLARE_ACCOUNT_ID`,
   `CLOUDFLARE_QUEUES_TOKEN`, and `QUIRE_QUEUE_PREFIX` if not `quire-`.
6. Run the companion host as in the [Vercel guide](/ops/vercel/), step 3.
   Migrations run there, before each Worker deployment.

A refused configuration shows in `bun run --bun wrangler tail` as "The web tier did
not start on cloudflare", followed by each setting to change.

Source: https://docs.quirelms.com/ops/cloudflare/index.mdx
