Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.6 — Preview Environments & Branch Deployments

For .NET engineers who know: Azure DevOps deployment slots, stage/UAT environments, and release pipelines with manual approval gates You’ll learn: How modern hosting platforms auto-provision isolated environments per pull request, and how to wire that into your QA workflow Time: 10-15 min read


The .NET Way (What You Already Know)

In the Azure world, environment promotion is a deliberate, configuration-heavy process. You create named environments in Azure DevOps (Development, Staging, Production), configure deployment slots in App Service, and wire up a release pipeline that promotes artifacts through gates. Staging slots share the same App Service Plan as production, which keeps them running, but spinning up a completely isolated environment for a single feature branch means provisioning another App Service, another database connection string, and another DNS entry — work typically done by hand or through ARM templates. The process is heavyweight enough that most teams skip branch-level environments entirely and rely on a shared QA slot that everyone deploys to in sequence.

The consequence is familiar: two developers are both testing features, one overwrites the shared QA environment, and someone ends up testing against the wrong code. “QA is broken” becomes a weekly conversation.


The Modern Hosting Way

Render and Vercel treat environment provisioning as a first-class, zero-configuration feature. Every pull request automatically gets its own deployed environment — its own URL, its own environment variables, its own isolated existence — and that environment is destroyed when the PR closes. No manual provisioning. No shared slots. No “QA is broken because Alex deployed his branch.”

The mental model shift: environments are cheap and ephemeral. Create one per branch, tear it down when the branch merges. QA happens on isolated previews, not on a shared staging slot.

Render Preview Environments

Render calls this feature Preview Environments. You enable it at the service level in the Render dashboard, and from that point on, every new branch pushed to GitHub with an open PR gets a separate deployment.

Enabling a preview environment:

In the Render dashboard, navigate to your service, open Settings, and toggle on Preview Environments. Render will show a preview environment tab listing all active branch deployments.

Your render.yaml — the Render Blueprint spec:

# render.yaml — infrastructure as code for Render
services:
  - type: web
    name: api
    runtime: node
    buildCommand: pnpm install && pnpm build
    startCommand: node dist/main.js
    envVars:
      - key: NODE_ENV
        value: production
      - key: DATABASE_URL
        fromDatabase:
          name: postgres-db
          property: connectionString
    previewsEnabled: true        # enables preview environments per PR
    previewsExpireAfterDays: 7   # auto-cleanup after 7 days

databases:
  - name: postgres-db
    databaseName: myapp
    user: myapp
    previewPlan: starter         # preview DBs use a cheaper plan

When previewsEnabled: true is set, Render provisions a separate database instance for each preview environment, running the schema migration on first deploy. This is the critical difference from Azure slots, which share a database by default.

Environment variables in preview environments:

Preview environments inherit the parent service’s environment variables by default. You can override specific variables for preview contexts:

envVars:
  - key: STRIPE_SECRET_KEY
    sync: false          # prompts for manual entry per environment
  - key: EMAIL_PROVIDER
    value: preview-stub  # override for previews — no real emails sent
  - key: LOG_LEVEL
    value: debug         # more verbose in preview

For sensitive keys (payment processors, external APIs), use sync: false to prevent preview environments from inheriting production credentials. Render prompts you to set these manually the first time a preview spins up.

The preview URL pattern:

Render generates URLs in the form https://api-pr-42-abc123.onrender.com. You can find the URL in the Render dashboard under the PR’s preview environment tab, and Render also posts it as a GitHub Deployment status, which appears directly on the PR.

Vercel Preview Deployments

If the frontend is on Vercel (Next.js), previews are on by default with zero configuration. Every push to any branch creates a deployment. The URL appears as a GitHub status check on the commit and PR.

# Vercel CLI — promote a preview to production manually
vercel promote https://myapp-abc123.vercel.app --token=$VERCEL_TOKEN

Branch-specific environment variables in Vercel:

Vercel lets you scope environment variables to environment type (Production, Preview, Development) and optionally to specific branches:

# Set an env var only for preview environments
vercel env add API_BASE_URL preview
# When prompted: https://api-preview.example.com

# Set an env var only for the `staging` branch
vercel env add FEATURE_FLAG_EXPERIMENTAL preview staging

In the Vercel dashboard: Settings > Environment Variables > add variable, then use the “Branch” field to scope it.


PR-Based Deployments in CI/CD

The GitHub Actions workflow that connects your PR to the preview environment:

# .github/workflows/preview.yml
name: Preview Deploy

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: pnpm test

      - name: Deploy to Render Preview
        # Render auto-deploys on push to the PR branch.
        # This step posts the preview URL as a PR comment.
        uses: render-oss/render-deploy-action@v1
        with:
          api-key: ${{ secrets.RENDER_API_KEY }}
          service-id: ${{ secrets.RENDER_SERVICE_ID }}

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            const previewUrl = process.env.RENDER_PREVIEW_URL;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `Preview environment ready: ${previewUrl}\n\nRuns against isolated database seeded from fixtures.`
            });

Our Preview Environment Workflow

The workflow this team follows:

  1. Engineer opens a PR. Render auto-deploys the branch within 3-5 minutes.
  2. GitHub posts the preview URL as a deployment status on the PR. The reviewer clicks the URL directly from GitHub.
  3. QA reviews on the preview URL. No access to staging needed.
  4. For features touching payments or external APIs, the preview uses stub/sandbox credentials scoped to that environment.
  5. PR merges. Render destroys the preview environment automatically (after previewsExpireAfterDays if set, or immediately on merge).
  6. Main branch deploys to production.

Database handling in preview environments:

Preview databases are provisioned from Render’s starter tier (lower cost), migrated on first deploy, and seeded via a seed script:

// package.json
{
  "scripts": {
    "db:seed:preview": "tsx prisma/seed-preview.ts"
  }
}
// prisma/seed-preview.ts — deterministic test data for QA
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // Only runs in non-production environments
  if (process.env.NODE_ENV === 'production') {
    throw new Error('Never run preview seed against production');
  }

  await prisma.user.createMany({
    data: [
      { email: 'admin@preview.test', role: 'ADMIN' },
      { email: 'user@preview.test', role: 'USER' },
    ],
    skipDuplicates: true,
  });

  console.log('Preview seed complete');
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

Add the seed script to your Render build command for preview environments:

# render.yaml
services:
  - type: web
    name: api
    buildCommand: pnpm install && pnpm build && pnpm db:migrate && pnpm db:seed:preview
    previewsEnabled: true

Key Differences

Azure DevOps / App ServiceRender / Vercel
Environments are provisioned manually or via ARM/BicepEnvironments auto-provision per PR, zero config
Deployment slots share the parent App Service PlanPreview environments are fully isolated services
Slots typically share the production database (with feature flags to isolate data)Render provisions a separate database per preview
Environment variables configured per slot in portalEnv vars inherited from parent, overridable per branch
Preview URL requires manual setup (Traffic Manager, CNAME)Preview URL is auto-generated and posted to the PR
Slot is persistent until manually deletedPreview is destroyed when the PR closes
Cost: slot runs continuously on shared planCost: preview databases have a small per-instance cost

Gotchas for .NET Engineers

1. Preview databases are real databases that cost money. Each preview environment on Render with previewPlan: starter incurs a small charge. If your team opens 10 PRs simultaneously and forgets to configure previewsExpireAfterDays, you accumulate idle database instances. Set the expiry, audit periodically with render list services, and close PRs promptly when abandoned.

2. Schema migrations run against the preview database on every deploy — including destructive ones. Render runs your build command (which includes prisma migrate deploy) on every push to the PR branch. If you push a migration that drops a column and then push a fix 10 minutes later, the column is gone. Preview databases are ephemeral, but mid-review destructive migrations will confuse QA reviewers who are mid-session. Use additive migrations for in-progress features (add the new column first, remove the old one in a later PR).

3. Environment variable inheritance catches people expecting isolation. Preview environments inherit all environment variables from the parent service unless you explicitly override them. If your parent service has STRIPE_SECRET_KEY set to the production key and you do not use sync: false for previews, your preview environment will attempt real Stripe charges. Always audit which credentials are inherited and whether that is safe for a PR environment a reviewer will click through.

4. The preview URL changes on every new Render deployment. Unlike Azure deployment slots, which have a stable .azurewebsites.net URL, Render preview URLs may rotate when the service is redeployed from scratch (e.g., after a configuration change). Do not embed preview URLs in external systems or test suites. Always retrieve the current URL from the Render dashboard or the GitHub Deployment status.

5. Vercel preview deployments do not share a backend. When the frontend is on Vercel and the backend is on Render, your Vercel preview deployment needs to point to the correct Render preview API URL. This requires the NEXT_PUBLIC_API_URL env var on the Vercel preview to be updated to match the Render preview URL. This is not automatic — you need a GitHub Actions step that queries the Render API for the preview URL and updates the Vercel environment variable, or a simpler approach: use a stable preview subdomain on your custom domain that always routes to the latest preview build.


Hands-On Exercise

Set up a complete preview environment workflow for a minimal NestJS + Next.js project.

Step 1: Add render.yaml to your repository root with previewsEnabled: true and a previewPlan: starter database.

Step 2: Create a prisma/seed-preview.ts that inserts 3 deterministic users and 5 test records. Add a guard at the top that throws if NODE_ENV === 'production'.

Step 3: Add the seed to the build command in render.yaml for preview contexts.

Step 4: Open a test PR against your repository. Verify that:

  • Render creates a new service instance visible in the dashboard
  • The GitHub PR shows a deployment status with the preview URL
  • Hitting the preview URL returns a 200 from the API health endpoint
  • The seed data is present (query /api/users and confirm your test users appear)

Step 5: Close the PR without merging. Confirm the preview environment is destroyed (check the Render dashboard after ~2 minutes).

Step 6: Add a GitHub Actions job that posts the preview URL as a PR comment. Test it by opening another PR.


Quick Reference

# View all active Render services (including previews)
render services list

# Trigger a manual deploy on Render (useful for debugging)
render deploys create --service-id srv-xxxx

# Check Vercel preview deployments for a project
vercel ls --token=$VERCEL_TOKEN

# Promote a specific Vercel preview to production
vercel promote <deployment-url> --token=$VERCEL_TOKEN

# List environment variables on a Vercel project (preview scope)
vercel env ls preview

# Add/update a Vercel env var for preview only
vercel env add MY_VAR preview

# Set Render env var via CLI
render env set MY_VAR=value --service-id srv-xxxx
# render.yaml — minimal preview environment config
services:
  - type: web
    name: api
    runtime: node
    buildCommand: pnpm install && pnpm build
    startCommand: node dist/main.js
    previewsEnabled: true
    previewsExpireAfterDays: 7
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: db
          property: connectionString
      - key: STRIPE_SECRET_KEY
        sync: false              # must be set manually per preview env

databases:
  - name: db
    previewPlan: starter

Further Reading