Stop Using Database Branching for PR Previews: Postgres Schemas Are Enough

Source: Dev.to | Jan 22, 2026 (UTC)

Isolated preview environments per PR using plain Postgres schemas – no database branching, no vendor lock-in.

View demo repo • See live examples

The Preview Environment Problem

Have you ever pushed a feature branch that needs database migrations, only to realize your preview deployment can’t actually test it?

We ran into this exact problem when adding a new table migration to a feature PR – the preview showed the UI but crashed on every query.

The dilemma:

  • Run migrations in a shared preview database? Breaks other developers’ PRs
  • Skip migrations? Can’t test your feature properly
  • Use production data? Absolutely not

The “obvious” solution is database branching services like Neon or PlanetScale. They’re excellent products, but they come with tradeoffs: vendor lock-in, another service to manage and pay for, migration complexity if you already have a Postgres database.

There’s a Simpler Way

What if I told you Postgres has had a built-in solution for 20+ years? Schemas.

Not database schemas in the abstract sense. Literal PostgreSQL schemas. Namespaces within a single database.

Database: myapp ├── Schema: public (production) ├── Schema: pr_15 (feature/add-auth) ├── Schema: pr_16 (bugfix/login) └── Schema: pr_17 (refactor/api)

Enter fullscreen mode

Exit fullscreen mode

Each PR gets its own isolated schema. Same database, separate data.

How It Works

The approach is dead simple:

  1. On preview build: Run migrations in schema pr_X (where X is the PR number)
  2. At runtime: Connect to schema pr_X
  3. On PR close: Drop schema pr_X

That’s it. No complex setup, no new services, no vendor lock-in.

The Key Insight

Modern ORMs (Prisma, MikroORM, TypeORM, Drizzle) all support schema parameters:

// Prisma datasources { db { url = "postgresql://user:pass@host/db?schema=pr_15" } } // MikroORM defineConfig({ schema: 'pr_15', driverOptions: { connection: { options: '-c search_path=pr_15' } } }) // TypeORM { schema: 'pr_15' }

Enter fullscreen mode

Exit fullscreen mode

You’re probably already one config option away from this working.

Implementation

Step 1: Compute Schema Name

Use your platform’s environment variables:

// Vercel const schemaName = process.env.VERCEL_GIT_PULL_REQUEST_ID ? `pr_${process.env.VERCEL_GIT_PULL_REQUEST_ID}` : 'public'; // Netlify const schemaName = process.env.REVIEW_ID ? `pr_${process.env.REVIEW_ID}` : 'public'; // Railway const schemaName = process.env.RAILWAY_GIT_BRANCH ? sanitizeBranchName(process.env.RAILWAY_GIT_BRANCH) : 'public';

Enter fullscreen mode

Exit fullscreen mode

Pro tip: Use PR numbers, not branch names. Branch names like feature/add-auth need sanitization. PR numbers are guaranteed unique and safe.

Step 2: Configure Your ORM

Pass the computed schema to your ORM:

// Prisma example const databaseUrl = process.env.DATABASE_URL; const schemaName = getSchemaName(); const url = `${databaseUrl}?schema=${schemaName}`; export const prisma = new PrismaClient({ datasources: { db: { url } } });

Enter fullscreen mode

Exit fullscreen mode

Step 3: Run Migrations in Build

Add to your build script:

{ "scripts": { "build": "npm run migrate && next build", "migrate": "prisma migrate deploy" } }

Enter fullscreen mode

Exit fullscreen mode

The magic here: when your ORM runs migrations with ?schema=pr_15, it automatically creates the schema if it doesn’t exist (depending on the ORM), then runs migrations inside it.

Step 4: Cleanup on PR Close

GitHub Action (works for any platform):

name: Cleanup Preview Schema on: pull_request: types: [closed] jobs: cleanup: runs-on: ubuntu-latest steps: - name: Drop schema run: | psql "${{ secrets.DATABASE_URL }}" \ -c "DROP SCHEMA IF EXISTS \"pr_${{ github.event.pull_request.number }}\" CASCADE"

Enter fullscreen mode

Exit fullscreen mode

Real Example: Next.js + MikroORM

Here’s a working implementation from kyryl-bogach/vercel-preview-schema-per-branch:

View source code • See live PRs with isolated previews

Schema computation:

// src/lib/schema-utils.ts export function getSchemaName(): string { // Production override if (process.env.DB_SCHEMA) { return process.env.DB_SCHEMA; } // PR number (preferred) if (process.env.VERCEL_GIT_PULL_REQUEST_ID) { return `pr_${process.env.VERCEL_GIT_PULL_REQUEST_ID}`; } // Local dev default return 'public'; }

Enter fullscreen mode

Exit fullscreen mode

ORM config:

// src/lib/mikro-orm.config.ts import { defineConfig } from '@mikro-orm/postgresql'; import { getSchemaName } from './schema-utils'; const schemaName = getSchemaName(); export default defineConfig({ entities: [Todo], clientUrl: process.env.DATABASE_URL, schema: schemaName, driverOptions: { connection: { options: `-c search_path=${schemaName}`, }, }, });

Enter fullscreen mode

Exit fullscreen mode

Build script:

{ "scripts": { "build": "npm run migrate && next build", "migrate": "mikro-orm migration:up" } }

Enter fullscreen mode

Exit fullscreen mode

Vercel environment variables:

That’s the entire setup. Three files, one environment variable, one GitHub secret.

Testing It Out

  1. Create a PR (e.g., PR #15)
  2. Vercel builds, migrations run in pr_15 schema
  3. Open preview URL, add some todos
  4. Create another PR (e.g., PR #16), add different todos
  5. Check both previews – completely isolated
  6. Close PR #15, schema pr_15 dropped automatically

Each preview has its own data. No conflicts, no shared state.

See it in action: Check out the demo PRs in the repo. Each has its own Vercel preview with isolated data.

The Honest Tradeoffs

This isn’t a silver bullet. Here’s what you’re trading:

What you gain:

What you lose:

When to use this:

When to use Neon/PlanetScale:

Why This Matters

Database branching services are great, but they shouldn’t be required for basic preview isolation.

Postgres schemas have existed since 1999. Your database already has this feature. Your ORM already supports it. You just need to wire it up.

This approach gives you:

Try It Yourself

I’ve built a working demo with Next.js + MikroORM showing the full implementation:

Repository: kyryl-bogach/vercel-preview-schema-per-branch

The repo includes:

Live examples: Browse the open PRs to see:

Clone it, deploy it to Vercel, open some PRs, and see isolated schemas in action.

Final Thoughts

Sometimes the best solution isn’t the newest service – it’s the 20-year-old feature hiding in plain sight.

Postgres schemas aren’t perfect for this use case (they were designed for multi-tenancy, not preview environments), but they’re good enough and work everywhere.

If you’re building on Vercel/Netlify/Railway with an existing Postgres database, give this approach a try. You might find it’s exactly the level of isolation you need, without the operational overhead of another service.

For further actions, you may consider blocking this person and/or reporting abuse

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Читайте також