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
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
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
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
Create a Vercel project from the repository with Root Directoryapps/web. apps/web/vercel.json sets the install and build commands
(NITRO_PRESET=vercel) and serves /sw.js uncached.
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.
On the companion host, run docker/compose.yaml without the web
service, with the same docker/.env:
Migrations run there, before each Vercel deployment is promoted.
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
Migrate from the companion host first, then promote the new Vercel
deployment, then roll the companion’s workers: the order in
upgrade.md. A Vercel instant rollback is a code rollback and is
always safe within a release.