<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Kyryl Bogach</title>
    <description>The latest articles on Forem by Kyryl Bogach (@kyryl-bogach).</description>
    <link>https://forem.com/kyryl-bogach</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3726973%2F1dbf451e-b1c2-4fa6-9061-d10f7c23d277.png</url>
      <title>Forem: Kyryl Bogach</title>
      <link>https://forem.com/kyryl-bogach</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kyryl-bogach"/>
    <language>en</language>
    <item>
      <title>Stop Using Database Branching for PR Previews: Postgres Schemas Are Enough</title>
      <dc:creator>Kyryl Bogach</dc:creator>
      <pubDate>Thu, 22 Jan 2026 19:46:02 +0000</pubDate>
      <link>https://forem.com/kyryl-bogach/stop-using-database-branching-for-pr-previews-postgres-schemas-are-enough-4mi8</link>
      <guid>https://forem.com/kyryl-bogach/stop-using-database-branching-for-pr-previews-postgres-schemas-are-enough-4mi8</guid>
      <description>&lt;p&gt;Isolated preview environments per PR using plain Postgres schemas - no database branching, no vendor lock-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch" rel="noopener noreferrer"&gt;View demo repo&lt;/a&gt; • &lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch/pulls" rel="noopener noreferrer"&gt;See live examples&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Preview Environment Problem
&lt;/h2&gt;

&lt;p&gt;Have you ever pushed a feature branch that needs database migrations, only to realize your preview deployment can't actually test it?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dilemma:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run migrations in a shared preview database? Breaks other developers' PRs&lt;/li&gt;
&lt;li&gt;Skip migrations? Can't test your feature properly&lt;/li&gt;
&lt;li&gt;Use production data? Absolutely not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's a Simpler Way
&lt;/h2&gt;

&lt;p&gt;What if I told you Postgres has had a built-in solution for 20+ years? &lt;strong&gt;Schemas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not database schemas in the abstract sense. Literal PostgreSQL schemas. Namespaces within a single database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database: myapp
├── Schema: public          (production)
├── Schema: pr_15           (feature/add-auth)
├── Schema: pr_16           (bugfix/login)
└── Schema: pr_17           (refactor/api)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each PR gets its own isolated schema. Same database, separate data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The approach is dead simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On preview build:&lt;/strong&gt; Run migrations in schema &lt;code&gt;pr_X&lt;/code&gt; (where X is the PR number)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At runtime:&lt;/strong&gt; Connect to schema &lt;code&gt;pr_X&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On PR close:&lt;/strong&gt; Drop schema &lt;code&gt;pr_X&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No complex setup, no new services, no vendor lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Key Insight
&lt;/h3&gt;

&lt;p&gt;Modern ORMs (Prisma, MikroORM, TypeORM, Drizzle) all support schema parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Prisma&lt;/span&gt;
&lt;span class="nx"&gt;datasources&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgresql://user:pass@host/db?schema=pr_15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// MikroORM&lt;/span&gt;
&lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pr_15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;driverOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c search_path=pr_15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// TypeORM&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pr_15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're probably already one config option away from this working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Compute Schema Name
&lt;/h3&gt;

&lt;p&gt;Use your platform's environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vercel&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERCEL_GIT_PULL_REQUEST_ID&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`pr_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERCEL_GIT_PULL_REQUEST_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Netlify&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REVIEW_ID&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`pr_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REVIEW_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Railway&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAILWAY_GIT_BRANCH&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sanitizeBranchName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAILWAY_GIT_BRANCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Step 2: Configure Your ORM
&lt;/h3&gt;

&lt;p&gt;Pass the computed schema to your ORM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Prisma example&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;databaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSchemaName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;databaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?schema=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;datasources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Run Migrations in Build
&lt;/h3&gt;

&lt;p&gt;Add to your build script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run migrate &amp;amp;&amp;amp; next build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"migrate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prisma migrate deploy"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic here: when your ORM runs migrations with &lt;code&gt;?schema=pr_15&lt;/code&gt;, it automatically creates the schema if it doesn't exist (depending on the ORM), then runs migrations inside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Cleanup on PR Close
&lt;/h3&gt;

&lt;p&gt;GitHub Action (works for any platform):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cleanup Preview Schema&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;closed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Drop schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;psql "${{ secrets.DATABASE_URL }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-c "DROP SCHEMA IF EXISTS \"pr_${{ github.event.pull_request.number }}\" CASCADE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real Example: Next.js + MikroORM
&lt;/h2&gt;

&lt;p&gt;Here's a working implementation from &lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch" rel="noopener noreferrer"&gt;kyryl-bogach/vercel-preview-schema-per-branch&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch" rel="noopener noreferrer"&gt;View source code&lt;/a&gt; • &lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch/pulls" rel="noopener noreferrer"&gt;See live PRs with isolated previews&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema computation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/schema-utils.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSchemaName&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Production override&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// PR number (preferred)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERCEL_GIT_PULL_REQUEST_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`pr_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERCEL_GIT_PULL_REQUEST_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Local dev default&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ORM config:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/mikro-orm.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mikro-orm/postgresql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getSchemaName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schema-utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSchemaName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Todo&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;clientUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;driverOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`-c search_path=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run migrate &amp;amp;&amp;amp; next build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"migrate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mikro-orm migration:up"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Vercel environment variables:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_URL&lt;/code&gt; = &lt;code&gt;postgresql://user:pass@host:5432/db&lt;/code&gt; (all environments)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DB_SCHEMA&lt;/code&gt; = &lt;code&gt;public&lt;/code&gt; (production only, forces specific schema)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire setup. Three files, one environment variable, one GitHub secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing It Out
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a PR (e.g., PR #15)&lt;/li&gt;
&lt;li&gt;Vercel builds, migrations run in &lt;code&gt;pr_15&lt;/code&gt; schema&lt;/li&gt;
&lt;li&gt;Open preview URL, add some todos&lt;/li&gt;
&lt;li&gt;Create another PR (e.g., PR #16), add different todos&lt;/li&gt;
&lt;li&gt;Check both previews - completely isolated&lt;/li&gt;
&lt;li&gt;Close PR #15, schema &lt;code&gt;pr_15&lt;/code&gt; dropped automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each preview has its own data. No conflicts, no shared state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See it in action:&lt;/strong&gt; Check out the &lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch/pulls" rel="noopener noreferrer"&gt;demo PRs&lt;/a&gt; in the repo. Each has its own Vercel preview with isolated data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Tradeoffs
&lt;/h2&gt;

&lt;p&gt;This isn't a silver bullet. Here's what you're trading:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works with any Postgres database&lt;/li&gt;
&lt;li&gt;No vendor lock-in&lt;/li&gt;
&lt;li&gt;Simple mental model (schemas, not "branches")&lt;/li&gt;
&lt;li&gt;One database to manage&lt;/li&gt;
&lt;li&gt;Standard SQL operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you lose:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual cleanup (GitHub Action can fail)&lt;/li&gt;
&lt;li&gt;Shared connection pool (all previews use same DB connections)&lt;/li&gt;
&lt;li&gt;Database-level objects are shared (extensions, types)&lt;/li&gt;
&lt;li&gt;Not true isolation (one DB, multiple namespaces)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to use this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have a Postgres database&lt;/li&gt;
&lt;li&gt;You want portability&lt;/li&gt;
&lt;li&gt;You have less than 10 concurrent PRs&lt;/li&gt;
&lt;li&gt;You're okay with "good enough" isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to use Neon/PlanetScale:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need true isolation&lt;/li&gt;
&lt;li&gt;You want zero-ops (automatic cleanup)&lt;/li&gt;
&lt;li&gt;You have 10+ concurrent PRs&lt;/li&gt;
&lt;li&gt;You need production-scale preview data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Database branching services are great, but they shouldn't be required for basic preview isolation.&lt;/p&gt;

&lt;p&gt;Postgres schemas have existed since 1999. Your database already has this feature. Your ORM already supports it. You just need to wire it up.&lt;/p&gt;

&lt;p&gt;This approach gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt; - Switch databases without rewriting your deployment pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; - One database, one connection string, schema parameter does the rest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt; - It's just SQL, you understand exactly what's happening&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;I've built a working demo with Next.js + MikroORM showing the full implementation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch" rel="noopener noreferrer"&gt;kyryl-bogach/vercel-preview-schema-per-branch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repo includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete working example with full source code&lt;/li&gt;
&lt;li&gt;Migration setup and schema utilities&lt;/li&gt;
&lt;li&gt;GitHub Action for automatic cleanup&lt;/li&gt;
&lt;li&gt;README (quick start) and CLAUDE.md (deep context)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live examples:&lt;/strong&gt; Browse the &lt;a href="https://github.com/kyryl-bogach/vercel-preview-schema-per-branch/pulls" rel="noopener noreferrer"&gt;open PRs&lt;/a&gt; to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each PR's Vercel preview deployment&lt;/li&gt;
&lt;li&gt;Build logs showing schema creation (&lt;code&gt;pr_2&lt;/code&gt;, &lt;code&gt;pr_3&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Isolated todo lists per preview&lt;/li&gt;
&lt;li&gt;Schema cleanup when PRs close&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clone it, deploy it to Vercel, open some PRs, and see isolated schemas in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Sometimes the best solution isn't the newest service - it's the 20-year-old feature hiding in plain sight.&lt;/p&gt;

&lt;p&gt;Postgres schemas aren't perfect for this use case (they were designed for multi-tenancy, not preview environments), but they're &lt;strong&gt;good enough&lt;/strong&gt; and &lt;strong&gt;work everywhere&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>vercel</category>
      <category>devops</category>
      <category>database</category>
    </item>
  </channel>
</rss>
