---
title: "Adding pull request previews"
description: "Give every pull request a preview of both applications at its own URL — before `development` deploys, after it, or without it."
canonical_url: "https://nuxtstart.com/docs/getting-started/adding-pull-request-previews"
---

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.

```mermaid
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](/docs/getting-started/deploying-development), after it, or without it.
[Pull request previews](/docs/deployment/pipeline#pull-request-previews) explains the mechanism; you
can finish this page without it.

Everything here starts on the `development` branch that
[Local setup](/docs/getting-started/local-setup) had you create.

<note>

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.

</note>

## Prerequisites

<table>
<thead>
  <tr>
    <th>
      You need
    </th>
    
    <th>
      How to get it
    </th>
    
    <th>
      Required
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Local setup finished
    </td>
    
    <td>
      <a href="/docs/getting-started/local-setup">
        Local setup
      </a>
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      The repository connected to Cloudflare
    </td>
    
    <td>
      <a href="/docs/getting-started/deploying-development#connect-the-repository-to-cloudflare">
        Connect the repository to Cloudflare
      </a>
      
      . Already done if <code>
        development
      </code>
      
       deploys
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      Your <code>
        workers.dev
      </code>
      
       subdomain
    </td>
    
    <td>
      The <strong>
        Workers & Pages
      </strong>
      
       page, beside <strong>
        Your subdomain
      </strong>
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      A Neon account
    </td>
    
    <td>
      <a href="https://console.neon.tech/signup" rel="nofollow">
        Sign up for Neon
      </a>
      
      . Pull request previews (<code>
        apps/web
      </code>
      
      ) need Neon: each pull request branches preview's Neon project
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      A DNS zone you control
    </td>
    
    <td>
      The value you set as <code>
        PROJECT_ZONE
      </code>
      
      . Add it to Cloudflare, or keep DNS access at your host
    </td>
    
    <td>
      Only for outgoing email
    </td>
  </tr>
  
  <tr>
    <td>
      Polar credentials for <code>
        preview
      </code>
    </td>
    
    <td>
      <a href="/docs/configuration/provider-accounts">
        Set up sign-in and payments
      </a>
      
       — a separate organization per context
    </td>
    
    <td>
      Only for payments
    </td>
  </tr>
</tbody>
</table>

## 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](https://neon.com/docs/reference/neon-cli),
signed in as an admin of your Neon organization:

```shell
# 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](https://console.neon.tech) 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](https://neon.com/docs/manage/api-keys#create-project-scoped-organization-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.

<caution>

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.

</caution>

### 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`:

```shell
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:

```shell
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](/docs/configuration/secret-store#what-to-put-in-the-local-folder) 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:

```env [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>
```

```env [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](/docs/deployment/pipeline#pull-request-previews)
describes.

<caution>

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.

</caution>

Social sign-in can't complete on a preview —
[Register these callback URLs](/docs/configuration/provider-accounts#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](/docs/reference/database#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](/docs/getting-started/deploying-development#email-will-not-send-until-you-onboard-a-domain)
walks it, reading the hostname with `APP_RUNTIME_CONTEXT=preview`.

## When it does not work

<table>
<thead>
  <tr>
    <th>
      What you see
    </th>
    
    <th>
      What to fix
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      A green run and no preview comment
    </td>
    
    <td>
      The deploy was skipped — <a href="/docs/deployment/pipeline#when-a-deploy-is-skipped">
        When a deploy is skipped
      </a>
      
       lists why
    </td>
  </tr>
  
  <tr>
    <td>
      Red at <code>
        Decide whether this run can deploy
      </code>
      
      , naming an origin
    </td>
    
    <td>
      <code>
        NUXT_PUBLIC_APP_BASE_URL
      </code>
      
       in that application's <code>
        .env.preview
      </code>
      
       is empty or not a bare <code>
        workers.dev
      </code>
      
       origin
    </td>
  </tr>
  
  <tr>
    <td>
      Red at <code>
        Decide whether this run can deploy
      </code>
      
      , naming a variable
    </td>
    
    <td>
      <code>
        INFISICAL_GH_PRS_IDENTITY_ID
      </code>
      
       is unset — <a href="/docs/getting-started/deploying-development#the-secret-and-the-variables">
        The secret and the variables
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      A red run naming a key
    </td>
    
    <td>
      That value is empty in <code>
        apps/*/.env.preview
      </code>
      
       or in the <code>
        development
      </code>
      
       tier's <code>
        /preview
      </code>
      
       folder
    </td>
  </tr>
  
  <tr>
    <td>
      The run stops at <code>
        Upsert the Cloudflare resources
      </code>
    </td>
    
    <td>
      <code>
        NEON_API_KEY
      </code>
      
       or <code>
        NEON_PROJECT_ID
      </code>
      
      , or one of the token's resource permissions
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Upsert the Cloudflare resources
      </code>
      
       fails creating a Neon branch or a Hyperdrive configuration
    </td>
    
    <td>
      Neon's branch cap or Cloudflare's Hyperdrive cap is reached. Close a pull request you no longer need, then rerun the job
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wrangler printed no Preview URL
      </code>
    </td>
    
    <td>
      Preview URLs are off on the preview Worker. Turn them on under that Worker's <strong>
        Domains & Routes
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        the preview serves … but the bundle was built for …
      </code>
    </td>
    
    <td>
      <code>
        NUXT_PUBLIC_APP_BASE_URL
      </code>
      
       in <code>
        .env.preview
      </code>
      
       names a Worker other than <code>
        <slug>-<app>-preview
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Google or GitHub sign-in fails on a preview
    </td>
    
    <td>
      Expected on every preview
    </td>
  </tr>
</tbody>
</table>

## Next steps

- [Pull request previews](/docs/deployment/pipeline#pull-request-previews) — where a preview's logs
are, what it binds, and how long it lives
- [Deploying development](/docs/getting-started/deploying-development) — put `development` on
Cloudflare, if you haven't
- [Adding staging](/docs/getting-started/adding-staging) — optional, a pre-production context between
`development` and production

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
