Skip to content

The web tier on Cloudflare Workers

Run a reduced Quire web tier on Cloudflare Workers.

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

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

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

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

  1. Create the resources:
    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:
    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:
    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):
    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, 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close