Getting Started

Adding pull request previews

Give every pull request a preview of both applications at its own URL — before `development` deploys, after it, or without it.

A pull request preview is both applications built from one pull request, served at a URL of its own, with a database of its own. By the end of this page, every pull request gets one, every push to it redeploys it, and closing it deletes it.

flowchart LR
    L["1. Local setup<br/>Both apps on your machine"] --> D["2. Deploying development<br/>A push deploys"]
    L --> R["**3. Adding pull request previews**<br/>Independent of development"]
    D --> S["4. Adding staging<br/>Optional"]
    S --> P["5. Going to production<br/>Your domain, real users"]
    D -. "without staging" .-> P

Previews stand on their own. Each pull request is a Cloudflare Worker Preview named pr-<n> on preview's own Worker, <slug>-<app>-preview, bound to preview's own bucket and content database. apps/web also gets a Preview Database: a Neon branch of preview's Neon project, migrated from empty by the pull request's own run. Nothing in a preview belongs to the development context, so this page works before Deploying development, after it, or without it. Pull request previews explains the mechanism; you can finish this page without it.

Everything here starts on the development branch that Local setup had you create.

You never type a Worker, bucket, database or pool name on this page. Each one follows from your PROJECT_SLUG, and the first pull request's run creates preview's Worker and resources.

Prerequisites

You needHow to get itRequired
Local setup finishedLocal setupYes
The repository connected to CloudflareConnect the repository to Cloudflare. Already done if development deploysYes
Your workers.dev subdomainThe Workers & Pages page, beside Your subdomainYes
A Neon accountSign up for Neon. Pull request previews (apps/web) need Neon: each pull request branches preview's Neon projectYes
A DNS zone you controlThe value you set as PROJECT_ZONE. Add it to Cloudflare, or keep DNS access at your hostOnly for outgoing email
Polar credentials for previewSet up sign-in and payments — a separate organization per contextOnly for payments

Add previews

1. Create preview's Neon project

Preview needs a Neon project of its own, never one another Runtime Context uses. Each pull request's run branches its default branch into a Preview Database pr-<n>, and closing the pull request deletes that branch. Create the project with the Neon CLI, signed in as an admin of your Neon organization:

# Creates the project; note the ID it prints.
neon projects create --name <slug>-web-preview --no-secrets
# Creates a key that reaches this one project; it prints the key once.
neon api-keys create --name <slug>-web-preview-github-actions --project-id <project id>

In the Neon Console instead, create the project with New Project on your organization's projects page. Then open your organization's Settings → API keys, click Create new, and select Project-scoped for that project. Manage API keys is the fallback when a screen moves.

Neon shows the key once, and you store it in the next step. A lost key means creating another and revoking the first.

Keep the project's default branch empty: no tables, no data, and its one neondb database. Every Preview Database starts as a copy of it, and the pull request's run migrates that copy from zero. Never point another context at this project, and never migrate its default branch.

2. Fill preview's secret folder

preview reads the development Secret Tier's /preview folder in apps/web's Infisical project — the project id is the projectId committed in apps/web/.env.infisical. Store the Neon key there as NEON_API_KEY:

infisical secrets set NEON_API_KEY='<the Neon key>' \
  --env=development --path=/preview --projectId='<apps/web project id>'

Store the sign-in for the Platform Admin seeded into every Preview Database beside it:

infisical secrets set SEED_PLATFORM_ADMIN_EMAIL='<user>@<org>.com' \
  SEED_PLATFORM_ADMIN_PASSWORD='<at least 8 characters>' \
  --env=development --path=/preview --projectId='<apps/web project id>'

The email must be a real address you own. Polar rejects reserved domains such as example.com, and password-reset mail goes to it. Every reviewer signs in with these two values.

Then add preview's own value for every key that Set up your secret store put in /local. A feature you're skipping takes a stand-in, as it does locally. Preview needs no DATABASE_URL: each pull request's run reads its Preview Database's connection string from Neon.

3. Write preview's values

Each application's .env.preview ships with these values empty. The origins are your preview Workers' own, built from your slug and the workers.dev subdomain:

apps/web/.env.preview
NUXT_PUBLIC_APP_BASE_URL=https://<slug>-web-preview.<subdomain>.workers.dev
NUXT_PUBLIC_APP_WEBSITE_BASE_URL=https://<slug>-site-preview.<subdomain>.workers.dev
GOOGLE_CLIENT_ID=<preview's Google client ID>
GITHUB_CLIENT_ID=<preview's GitHub client ID>
NEON_PROJECT_ID=<the project ID from step 1>
apps/site/.env.preview
NUXT_PUBLIC_APP_BASE_URL=https://<slug>-site-preview.<subdomain>.workers.dev
NUXT_PUBLIC_CONTACT_EMAIL=hello@example.com

NEON_PROJECT_ID is the project's ID, such as green-breeze-12345678, from the project's Settings page. The br-… ID beside it names a branch, and the schema rejects it.

No pull request is served from these origins: the workflow puts pr-<n>- in front of each one before the build, as Pull request previews describes.

Keep each origin a bare https://…workers.dev value — no quotes, no trailing comment. Anything else ends the run red at Decide whether this run can deploy. NUXT_PUBLIC_APP_BASE_URL must also name the Worker your slug builds, <slug>-<app>-preview, or Deploy the pull request preview ends red because the preview's URL and the bundle's origin disagree.

Social sign-in can't complete on a preview — Register these callback URLs says why — so Google and GitHub take stand-ins here at no cost, in both client IDs and in their client secrets in /preview.

4. Open a pull request

Commit both files on a new branch off development, push it, and open a pull request into development. Its first run creates preview's Worker, bucket and content database, each named <slug>-<app>-preview, and every later pull request finds them. It also creates the pull request's Preview Database, the Neon branch pr-<n>, behind the Hyperdrive configuration <slug>-web-preview-pr-<n>, and migrates and seeds it before the upload.

The workflow comments on the pull request with each application's preview URL, https://pr-<n>-<slug>-<app>-preview.<subdomain>.workers.dev. Every push to the pull request redeploys that preview and migrates its Preview Database. Closing the pull request, merged or not, deletes both.

Sign in to the apps/web preview as the seeded Platform Admin, with SEED_PLATFORM_ADMIN_EMAIL and SEED_PLATFORM_ADMIN_PASSWORD from step 2. Seed data explains how to add rows to the seed.

5. Onboard preview's sending domains

Previews send from <slug>-<app>-preview.<zone>, one hostname per application, shared by every pull request. Onboard each one the way Email will not send until you onboard a domain walks it, reading the hostname with APP_RUNTIME_CONTEXT=preview.

When it does not work

What you seeWhat to fix
A green run and no preview commentThe deploy was skipped — When a deploy is skipped lists why
Red at Decide whether this run can deploy, naming an originNUXT_PUBLIC_APP_BASE_URL in that application's .env.preview is empty or not a bare workers.dev origin
Red at Decide whether this run can deploy, naming a variableINFISICAL_GH_PRS_IDENTITY_ID is unset — The secret and the variables
A red run naming a keyThat value is empty in apps/*/.env.preview or in the development tier's /preview folder
The run stops at Upsert the Cloudflare resourcesNEON_API_KEY or NEON_PROJECT_ID, or one of the token's resource permissions
Upsert the Cloudflare resources fails creating a Neon branch or a Hyperdrive configurationNeon's branch cap or Cloudflare's Hyperdrive cap is reached. Close a pull request you no longer need, then rerun the job
wrangler printed no Preview URLPreview URLs are off on the preview Worker. Turn them on under that Worker's Domains & Routes
the preview serves … but the bundle was built for …NUXT_PUBLIC_APP_BASE_URL in .env.preview names a Worker other than <slug>-<app>-preview
Google or GitHub sign-in fails on a previewExpected on every preview

Next steps

Copyright © 2026