---
title: "The web tier on Vercel"
description: "Run the Quire web tier on Vercel with a companion worker."
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 Vercel

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

The design is `docs/architecture/23-ops.md` section 5. Vercel runs the web
tier only. Everything else runs on a companion host you operate, and it is
not optional.

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

The configuration is in place (`apps/web/vercel.json`, the `vercel` Nitro
preset and the start-up checks). Vercel needs shared file storage
(`QUIRE_STORAGE_DRIVER=s3` or `azure`), a realtime driver that works across
function instances (`QUIRE_REALTIME_DRIVER=sse` or `centrifugo`) and an HTTP
email provider. Without them the web tier refuses to start and its log
names each missing setting.

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

| Piece | Where it runs |
| --- | --- |
| `web` | Vercel Functions, Bun runtime: pages, REST, MCP, LTI, webhooks received |
| `worker`, `scheduler`, `collab`, `content` | A companion host: Fly, Railway, ECS, or your own Docker host with `docker/compose.yaml` |
| Postgres | External, behind a transaction pooler: Neon, Supabase, or RDS with PgBouncer |
| Files | S3 or R2. There is no persistent disk |
| Background jobs | Enqueued with pg-boss inside the request's transaction, run by the companion worker. With `QUIRE_QUEUE_DRIVER=vercel` on the companion worker, the light jobs (unordered notification and webhook deliveries) go through Vercel Queues (`VERCEL_QUEUE_REGION`, `VERCEL_QUEUE_TOKEN`), so the pooled Postgres is not polled for them |
| Recurring jobs | The companion scheduler. There is no Vercel Cron route, because a cron that does work inside a function times out |
| Tracing | OpenTelemetry to your collector, `OTEL_EXPORTER_OTLP_ENDPOINT` |

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

The web tier checks these when it starts and refuses, naming every problem,
rather than failing later at the first email that never sends:

- **No SMTP.** Vercel blocks outbound SMTP. Set `QUIRE_EMAIL_PROVIDER_CONFIG`
  to an HTTP provider (Postmark, SES, Mailgun, SendGrid or Resend).
  `QUIRE_SMTP_URL` is refused.
- **No local disk.** `QUIRE_STORAGE_DRIVER=local` is refused.
- **No shared memory between invocations.** `QUIRE_REALTIME_DRIVER=inprocess`
  is refused. Server-sent events are capped at the function timeout; the
  client reconnects with a cursor, so no event is lost, but presence is
  unavailable.
- **Pinned tenant databases are limited** to about eight, because each one is
  another connection pool in an environment that cannot share pools.

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

1. Create a Vercel project from the repository with **Root Directory**
   `apps/web`. `apps/web/vercel.json` sets the install and build commands
   (`NITRO_PRESET=vercel`) and serves `/sw.js` uncached.
2. Set the environment variables. From `docker/.env.example`, at least:
   `QUIRE_DEPLOY_TARGET=vercel`, `QUIRE_APP_ORIGIN`, `QUIRE_CONTENT_ORIGIN`,
   `QUIRE_PLATFORM_DOMAINS`, `QUIRE_SECRET_KEY`, `QUIRE_MASTER_KEY`,
   `DATABASE_URL` (the pooler's address), `QUIRE_REPORT_DATABASE_URL`,
   `QUIRE_AUDIT_DATABASE_URL`, `QUIRE_DATABASE_ID`,
   `QUIRE_EMAIL_PROVIDER_CONFIG`, `QUIRE_MAIL_FROM`, the storage settings,
   and `QUIRE_COLLAB_URL` and `QUIRE_COLLAB_SIGNING_KEY` pointing at the
   companion's collab service.
   `QUIRE_REPORT_DATABASE_URL` belongs to the physical database named by
   `DATABASE_URL`. For each additional registered database, add its own
   `quire_report` connection URL to both web and worker environments, then
   register its environment variable name as `env:NAME` in the platform
   console. Analytics fails closed if that database's report URL is missing.
3. On the companion host, run `docker/compose.yaml` without the `web`
   service, with the same `docker/.env`:
   ```sh
   docker compose -f docker/compose.yaml up -d migrate init worker scheduler collab content
   ```
   Migrations run there, before each Vercel deployment is promoted.
4. Deploy. On a refused configuration the function log starts with "The web
   tier did not start on vercel" and lists each setting to change.

## Upgrading <!--quire:upgrading-->

Migrate from the companion host first, then promote the new Vercel
deployment, then roll the companion's workers: the order in
[upgrade.md](/ops/upgrade/). A Vercel instant rollback is a code rollback and is
always safe within a release.

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