We used to review pull requests entirely in the GitHub UI. Read the diff, mentally simulate what the change would look like, approve it, merge it, and then find out on staging that the dropdown menu was completely broken on mobile. Classic. Preview deployments exist specifically to prevent that kind of embarrassment, and yet a lot of teams treat them as a passive thing — they exist, Vercel creates them automatically, and nobody actually clicks the link.
After shipping enough features that looked fine in code review but were obviously broken the second you loaded them in a browser, we got serious about making preview deployments a core part of how we work. Here's what that actually looks like in practice.
What a Preview Deployment Actually Is (and Isn't)
A preview deployment is an isolated, shareable URL that reflects a specific branch or pull request — not your main branch, not your production environment. On Vercel, every push to a non-production branch gets one automatically. Netlify does the same. The URL is unique per commit or PR, it stays up as long as you need it, and it's accessible to anyone with the link.
What it isn't: a staging environment. Staging is a persistent, shared environment that's supposed to mirror production. Preview deployments are ephemeral, isolated, and per-branch. The distinction matters because they serve different purposes. Staging is where you do final QA before a release. Preview deployments are where you catch problems before they even get to staging.
Preview deployments should be the first place anyone looks at a change — before code review is even complete, not after.
The Bare Minimum Setup That Most Teams Skip
If you're on Vercel with a Next.js project, preview deployments just work out of the box. But 'just working' and 'being useful' are two different things. The most common failure mode is that the preview deployment hits your production database, your production Stripe account, or some other real service — which means either the preview is dangerous to test on or it's showing you inaccurate data.
You want your preview deployments to use test/development versions of all your external services. In Vercel, you can scope environment variables by environment: Production, Preview, and Development are separate. Use them.
# In your Vercel project settings, you'd set these per environment.
# For Preview environments specifically:
DATABASE_URL=postgresql://user:pass@preview-db.example.com/myapp_preview
STRIPE_SECRET_KEY=sk_test_your_test_key_here
NEXTAUTH_URL=https://${VERCEL_URL} # Vercel injects this automatically
NEXT_PUBLIC_APP_URL=https://${VERCEL_URL}That VERCEL_URL environment variable is worth calling out. Vercel injects it automatically with the unique URL for each preview deployment. If you hardcode your app URL anywhere in your env vars for preview, authentication redirects will break because the URL changes per deployment. Use VERCEL_URL instead, or better yet, make sure your code handles dynamic base URLs gracefully.
Database Branching: The Part That Makes This Actually Work
Here's the problem with a shared preview database: two developers open PRs at the same time, both run migrations, and now neither preview deployment has a valid schema. It's a mess. The real solution is database branching — spinning up an isolated copy of your database schema per preview deployment.
Neon (our preferred Postgres provider for this kind of thing) has built-in branching that integrates with Vercel directly. When you connect Neon to your Vercel project, it can automatically create a database branch for each preview deployment and tear it down when the PR closes. This is genuinely one of those features that feels like magic the first time you use it.
// vercel.json — you can run setup commands per environment
// This runs your Prisma migrations on each preview deployment
{
"buildCommand": "prisma generate && prisma migrate deploy && next build",
"ignoreCommand": "git diff --quiet HEAD^ HEAD -- . ':!public' ':!**.md'"
}
// Or if you want more control, use a custom build script:
// package.json
{
"scripts": {
"build": "next build",
"build:preview": "prisma migrate deploy && next build",
"build:production": "prisma migrate deploy && next build"
}
}If Neon isn't your thing, PlanetScale has a similar branching concept, and you can always script your own solution with something like a Vercel deployment webhook that triggers database provisioning. It's more work, but once it's set up you forget about it.
Making Preview Deployments a Required Step in Code Review
The cultural change matters as much as the technical setup. We made it a rule: if your PR changes anything visual or any user-facing flow, the reviewer has to click the preview link and actually test it before approving. Not just read the code — open the deployment, click through the thing you changed.
To make this frictionless, we added a PR template that includes the preview URL field and a checklist. GitHub doesn't auto-populate the Vercel preview URL in your PR description, but the Vercel bot comment does add it. You can also use the Vercel GitHub integration to add commit status checks, so the preview URL is visible right in the PR.
<!-- .github/pull_request_template.md -->
## What does this PR do?
<!-- Brief description -->
## Preview Deployment
<!-- Paste the Vercel preview URL here once it's ready -->
## Testing Checklist
- [ ] Opened the preview deployment (not just reviewed the code)
- [ ] Tested on mobile viewport (resize browser or use DevTools)
- [ ] Checked the happy path works
- [ ] Checked at least one error/edge case
- [ ] Ran through the flow as a logged-out user if relevant
## Screenshots or Screen Recording
<!-- Required for any UI changes -->That last bullet — testing as a logged-out user — has caught more bugs than everything else combined. Auth state is one of those things that's almost impossible to reason about from a diff alone.
Sharing Preview Deployments With Non-Technical Stakeholders
One underrated use of preview deployments: showing work-in-progress to designers, product managers, or clients without any of the 'let me share my screen' overhead. You have a URL. You send it. They click it. Done.
The catch is authentication. If your entire app is behind a login, the preview deployment is useless to someone who doesn't have an account. A few solutions here: first, make sure your test/preview environment has some seed data with easy-to-share test credentials. Second, consider adding a bypass or demo mode for specific routes. Third, Vercel has a 'Password Protection' feature for preview deployments — you can set a single password that lets anyone past the login screen of the deployment itself, separate from your app's auth.
// Seed your preview database with demo accounts
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
import { hash } from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
// Only seed if we're not in production
if (process.env.NODE_ENV === 'production') {
console.log('Skipping seed in production')
return
}
const demoPassword = await hash('demo1234', 12)
await prisma.user.upsert({
where: { email: 'demo@example.com' },
update: {},
create: {
email: 'demo@example.com',
name: 'Demo User',
password: demoPassword,
// Add whatever your schema needs
},
})
console.log('Demo user seeded: demo@example.com / demo1234')
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect())Include the demo credentials in your PR description when you share the preview link. Designers shouldn't have to ask you for a password to review your work.
Automated Checks That Run Against Preview Deployments
Preview deployments get even more powerful when you run automated checks against them. The URL is predictable once you know your Vercel project name and the branch, so you can point tools at it.
Playwright and Cypress both support running end-to-end tests against a remote URL. You can set up a GitHub Action that waits for the preview deployment to be ready, extracts the URL from the Vercel deployment, and runs your E2E suite against it. This is the closest thing to 'automated QA' without building an entire QA pipeline.
# .github/workflows/preview-e2e.yml
name: E2E Tests on Preview
on:
deployment_status:
jobs:
e2e:
if: github.event.deployment_status.state == 'success' && github.event.deployment_status.environment == 'Preview'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get Preview URL
id: preview
run: echo "url=${{ github.event.deployment_status.target_url }}" >> $GITHUB_OUTPUT
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: ${{ steps.preview.outputs.url }}
TEST_USER_EMAIL: demo@example.com
TEST_USER_PASSWORD: demo1234The deployment_status event is the key here — it fires when Vercel marks a deployment as ready, which means your tests don't start until there's actually something to test. We wasted a few hours figuring this out before landing on it.
- Lighthouse CI: run it against your preview to catch performance regressions before they hit production
- Axe or Pa11y: automated accessibility checks against the real rendered output
- Broken link checkers: surprisingly useful for content-heavy pages
- Visual regression tools like Percy or Chromatic: screenshot diffs against a baseline
The Practical Workflow That Actually Sticks
Theory is nice. Here's the actual workflow we use, which is simple enough that it doesn't feel like overhead:
- Push a branch → Vercel creates a preview deployment automatically. Takes 1-3 minutes for a typical Next.js build.
- Open your own PR → paste the preview URL in the description, add a screenshot if it's UI work.
- Request review → the reviewer is expected to click the link, not just read the diff.
- For any PR that touches auth, payments, or a critical user flow → at least one person walks through it as an end user.
- Automated E2E tests run against the preview URL via GitHub Actions.
- Merge only after the preview has been signed off — eyes on the actual running code, not just the diff.
This sounds like more process than it is. In practice, 'click the preview link' adds maybe 2-3 minutes to a code review. The time it saves when you catch a broken flow before it gets merged is easily 10x that.
If you're using one of the peal.dev templates, this workflow slots in cleanly — the templates are all built with Vercel deployment in mind, with separate environment variable handling for preview vs. production already thought through. It's one less thing to figure out when you're trying to get a project off the ground.
The goal of a preview deployment isn't to have a URL. It's to make 'does this actually work?' a question anyone can answer in 30 seconds, without asking a developer to set anything up.
One last thing: clean up your preview deployments. If you're using database branching, make sure PR-closed webhooks actually tear down the branches, otherwise you accumulate database instances and storage costs. Vercel handles the deployment cleanup automatically, but your database provider usually doesn't. Write a cleanup script and hook it to the pull_request closed event in GitHub Actions. You'll thank yourself when you get your cloud bill.
