Skip to Content
Self-hostingDeploying to Vercel

Deploying to Vercel

Before you start, have the services created and your environment variables ready.

Import the repository

Create a new Vercel project from your fork of the monorepo and set its Root Directory to apps/app. The framework preset is Next.js.

Connect storage

On the project’s Storage tab, connect two Blob stores: a public one with the env var prefix PUBLIC_BLOB, which injects PUBLIC_BLOB_READ_WRITE_TOKEN, and a private one with the prefix PRIVATE_BLOB, which injects PRIVATE_BLOB_READ_WRITE_TOKEN (see File store). Vercel Sandbox authenticates with Vercel’s OIDC token, which is also injected automatically.

Push the sandbox image

Chat sandboxes boot from a custom image in the project’s Vercel Container Registry. Set up the Vercel Sandbox image workflow or push it by hand, as Sandbox image describes.

Add environment variables

Add each variable in the right scope. See the table below.

Deploy

The build runs drizzle-kit migrate against DATABASE_URL, then next build, so schema migrations land before the new code serves traffic.

Point your domain

Add your custom domain. BETTER_AUTH_URL and BETTER_AUTH_PRODUCTION_URL must match it.

Environment variable scopes

Preview deploys sign users in through production: the auth proxy signs state on production and verifies it on the preview that started the sign-in. That’s why some values must be identical everywhere.

VariableScopeWhy
BETTER_AUTH_URLProduction onlyPreviews derive their own URL from VERCEL_URL.
BETTER_AUTH_PRODUCTION_URL, BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRETProduction + Preview, identical valuesRequired by the preview sign-in proxy.
NEXT_PUBLIC_BASE_PATHProduction + PreviewOnly if you serve under a path prefix.
CRON_SECRETProductionVercel Cron only runs on production. See below.
Everything else (DATABASE_URL, LIVEBLOCKS_SECRET_KEY, model keys, AGENT_DEFAULT_MODEL, ENCRYPTION_KEY, TERMINAL_AUTH_SECRET, SANDBOX_HARNESSES, …)Production + Preview

Preview deploys run migrations too. If Preview points at the production database, a PR’s migration reaches production before the PR merges. To keep them apart, give previews their own database. For example, Neon’s Vercel integration creates a database branch per preview automatically.

Pull request checks

The app’s vercel.json adds a Vercel Cron job that calls /app/api/pr-watch/tick every five minutes. It checks each canvas with an open pull request on GitHub and adds what changed (failing checks, a conflict, the merge) to the chat, even when no one has the canvas open. Set CRON_SECRET in Production so the route accepts the call. If you serve the app at the domain root rather than under /app, change the job’s path to /api/pr-watch/tick. Vercel’s Hobby plan only runs cron jobs once a day, so the job needs a Pro plan.

Preview deploys are opt-in

Each app’s vercel.json sets an ignored build step (scripts/vercel-ignore-build.sh) so previews don’t spend build credits on every push. Production deploys always build. A preview builds only when you opt in, for every app or for one:

Opt in withBuilds
preview label on the PRapp, docs and homepage
preview:app, preview:docs or preview:homepage labelthat app only
[preview] in the pushed commit’s subject lineapp, docs and homepage
[preview:app], [preview:docs] or [preview:homepage] in the subject linethat app only

Adding a label doesn’t start a deploy on its own: push afterwards, or redeploy from Vercel. Only the subject line counts, so a commit body that mentions a token doesn’t opt in.

The label check calls the GitHub API unauthenticated, which works for a public repository. For a private fork, add a GITHUB_TOKEN environment variable (Preview scope) with read access to its pull requests.

Serving under a path

The reference deployment serves the marketing site at the domain root, the product at /app, and these docs at /docs. The marketing app (apps/homepage/vercel.json) rewrites /app/* and /docs/* to the other two Vercel projects, and the product project sets NEXT_PUBLIC_BASE_PATH=/app. To serve Screenplay from its own domain instead, leave NEXT_PUBLIC_BASE_PATH unset. See Mount path.

Last updated on