How to Set Up Preview Deployments for Every Pull Request
In modern software engineering, the traditional concept of a single, shared "staging" environment is rapidly becoming a bottleneck. When multiple developers work on parallel feature branches, they inevitably collide on staging, leading to overwritten databases, configuration drift, and blocked testing cycles.
Preview deployments—often referred to as ephemeral staging environments—solve this by generating a unique, isolated, and fully functional version of your application for every pull request. This allows product managers, QA engineering groups, and peer reviewers to validate changes in a production-like setting before they are merged into the main branch.
Building an automated preview pipeline requires coordinating several architectural pieces: triggering builds on version control events, deploying static assets or serverless functions to a CDN or edge network, provisioning database schema branches, and posting the deployment URL back to the developer's pull request. In this guide, we will design a robust, secure preview deployment workflow using GitHub Actions and modern edge hosting, complete with ephemeral database synchronization and automated cleanup configurations.
The Ephemeral Architecture Design
To implement preview deployments effectively, we must move away from heavy, virtual-machine-based infrastructure and leverage lightweight, serverless, or containerized hosting. The architecture typically consists of three pillars:
- The CI/CD Trigger: A GitHub Actions workflow that responds to
pull_requestevents (such as opened, synchronized, or reopened). - The Compute & Hosting Layer: An edge network (like Cloudflare Pages or Vercel) or a lightweight container orchestration platform (like AWS ECS or Fly.io) that can spin up environments near-instantaneously.
- The Ephemeral Data Store: A database branching mechanism (e.g., using Neon's serverless Postgres branching or PlanetScale's schema branches) to provide each preview environment with its own isolated database state.
Step-by-Step GitHub Actions Workflow
Let's construct a GitHub Actions workflow that compiles a frontend and API application, deploys them to an edge hosting target, and updates the pull request description with a direct link. Below is a production-grade YAML definition showcasing how to deploy a modern frontend stack using CLI tools:
name: Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
deploy_preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Build Application
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ steps.db_branch.outputs.api_url }}
- name: Deploy to Edge Hosting
id: deploy
run: |
npx wrangler pages deploy ./dist --project-name="my-app" --branch="${{ github.head_ref }}" > deploy_log.txt
PREVIEW_URL=$(grep -oE "https://[a-zA-Z0-9.-]+\.pages\.dev" deploy_log.txt | head -n 1)
echo "preview_url=$PREVIEW_URL" >> $GITHUB_OUTPUT
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Comment PR with Preview Link
uses: actions/github-script@v7
with:
script: |
const url = "${{ steps.deploy.outputs.preview_url }}";
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Preview Deployment Ready!\n\nYou can access your staging environment here: [${url}](${url})\n\n_Built automatically via GitHub Actions._`
});
cleanup_preview:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Delete Preview Branch Environment
run: |
echo "Tearing down preview environment for branch: ${{ github.head_ref }}"
# Call wrangler CLI or Cloudflare API to delete Pages preview deployment
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Handling Database State and Branching
The biggest hurdle in preview deployments is managing the database. Connecting fifty preview builds to a single staging database leads to schema conflicts and data corruption. There are two primary solutions to this problem:
- Mock API Servers: For simple frontend UI verification, running a mock API server (using tools like MSW or wiremock) can simulate backend responses without needing a database. This is clean and runs fast, but lacks full-stack database testing.
- Database Branching: For full-stack verification, databases that support logical branching (such as Neon Postgres or PlanetScale) are ideal. You can use their API within the CI/CD pipeline to clone the main database's schema and seeding data into a temporary branch, run your migrations against that branch, point the preview frontend to it, and drop the branch when the PR is closed.
To implement database branching, you invoke the provider's CLI or REST API. In the GitHub Actions file, you trigger a branch creation step before compiling the application.
This step yields a temporary database URL, which is then injected as an environment variable during the compile phase. This ensures that every preview environment has its own sandbox database containing a fresh copy of your production structure and sample data, letting you test migrations in isolation.
Additionally, you must consider database migration failures. If a migration fails on a preview branch, the CI/CD pipeline should halt and report the migration log back to the pull request.
This feedback loop is essential because it alerts the developer to schema conflicts before code is merged. Once the pull request is closed or merged, you should trigger a cleanup hook to drop the temporary database branch, avoiding resource leaks and keeping your cloud database bill tidy.
Securing Your Preview Environments
Because preview environments contain unfinished features and potentially sensitive debugging utilities, they should not be accessible to the general public. Securing them requires implementing lightweight authentication.
You can leverage edge middleware to intercept incoming requests and verify credentials, or wrap the deployment under an identity provider tunnel like Cloudflare Access or Tailscale. This ensures that only authenticated team members can view the staging previews, while keeping the build speed and accessibility optimized.
Another option is implementing basic HTTP authentication via edge handlers. Edge functions sit between the client and the asset hosting layer, validating incoming headers before serving files.
This is a low-overhead security system that doesn't require maintaining full authentication servers. For enterprise setups, integrating Single Sign-On (SSO) with your organization's Okta or Google workspace ensures that internal releases remain strictly confidential.
Furthermore, CORS configuration is a major challenge for dynamic staging sites. Since each preview environment generates a different subdomain (e.g., https://pr-123.my-app.pages.dev), your backend API must be configured to accept dynamic origins. To avoid opening CORS to the entire web, your API middleware should validate the incoming origin against a regular expression pattern that matches only your trusted preview subdomain format, ensuring both usability and API protection.
Deploying preview builds at the edge with Bramsley
Designing and keeping an automated preview workflow running smoothly is highly complex, demanding deep expertise in cloud architectures, serverless runtimes, and CI/CD pipelines. At Bramsley Digital Studio, we help product teams and engineering organizations architect these exact high-performance, edge-first preview infrastructures.
By utilizing advanced serverless computing engines, dynamic routing, and logical database branching, We enable developers to deploy previews in milliseconds with comprehensive security and zero environment drift. Partner with us to modernize your deployment practices and eliminate testing bottlenecks at the global edge.