<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://docs.postkitstack.com/blog</id>
    <title>PostKit Blog</title>
    <updated>2026-05-12T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://docs.postkitstack.com/blog"/>
    <subtitle>PostKit Blog</subtitle>
    <icon>https://docs.postkitstack.com/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Migrating from Supabase to PostKit: A Practical Guide]]></title>
        <id>https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit</id>
        <link href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit"/>
        <updated>2026-05-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Supabase is a great way to get a PostgreSQL database running in minutes. But as your project grows, you often outgrow the auto-generated migrations and want more control over exactly what SQL runs against your database. PostKit gives you that control — full schema-as-code, session-based migrations, and a deploy pipeline you can reason about.]]></summary>
        <content type="html"><![CDATA[<p>Supabase is a great way to get a PostgreSQL database running in minutes. But as your project grows, you often outgrow the auto-generated migrations and want more control over exactly what SQL runs against your database. PostKit gives you that control — full schema-as-code, session-based migrations, and a deploy pipeline you can reason about.</p>
<p>This guide walks you through bringing a Supabase project into PostKit's migration workflow without disrupting your existing data.</p>
<p><img decoding="async" loading="lazy" alt="Migrating from Supabase to PostKit" src="https://docs.postkitstack.com/assets/images/supabase-to-postkit-3010069ed9340098ed5298b0b05589f3.jpg" width="1168" height="784" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-changes-and-what-doesnt">What Changes (and What Doesn't)<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#what-changes-and-what-doesnt" class="hash-link" aria-label="Direct link to What Changes (and What Doesn't)" title="Direct link to What Changes (and What Doesn't)" translate="no">​</a></h2>
<table><thead><tr><th></th><th>Supabase</th><th>PostKit</th></tr></thead><tbody><tr><td>Database</td><td>PostgreSQL</td><td>PostgreSQL (same!)</td></tr><tr><td>Schema management</td><td>Supabase Studio / SQL editor</td><td>Schema files in <code>db/schema/</code></td></tr><tr><td>Migration files</td><td>Auto-generated by Studio</td><td>pgschema-diffed, hand-reviewable</td></tr><tr><td>Deployment</td><td>Studio apply / CLI</td><td><code>postkit db deploy</code> with dry-run</td></tr><tr><td>Local dev</td><td>Supabase local stack</td><td>Docker container (auto, version-matched)</td></tr></tbody></table>
<p>PostKit does not replace Supabase's auth, storage, or realtime features. If you use PostgREST/Supabase's auto-API, PostKit manages the underlying schema while your application code stays the same.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-install-postkit">Step 1: Install PostKit<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#step-1-install-postkit" class="hash-link" aria-label="Direct link to Step 1: Install PostKit" title="Direct link to Step 1: Install PostKit" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">npm install -g @appritech/postkit</span><br></span></code></pre></div></div>
<p>Verify:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit --version</span><br></span></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-initialize-postkit-in-your-project">Step 2: Initialize PostKit in Your Project<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#step-2-initialize-postkit-in-your-project" class="hash-link" aria-label="Direct link to Step 2: Initialize PostKit in Your Project" title="Direct link to Step 2: Initialize PostKit in Your Project" translate="no">​</a></h2>
<p>In your project root:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit init</span><br></span></code></pre></div></div>
<p>This creates:</p>
<ul>
<li class=""><code>postkit.config.json</code> — committed, non-sensitive settings</li>
<li class=""><code>postkit.secrets.json</code> — gitignored, your credentials</li>
<li class=""><code>postkit.secrets.example.json</code> — template for teammates</li>
<li class=""><code>db/schema/</code> — where your schema files will live</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-add-your-supabase-database-as-a-remote">Step 3: Add Your Supabase Database as a Remote<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#step-3-add-your-supabase-database-as-a-remote" class="hash-link" aria-label="Direct link to Step 3: Add Your Supabase Database as a Remote" title="Direct link to Step 3: Add Your Supabase Database as a Remote" translate="no">​</a></h2>
<p>Your Supabase project has a direct PostgreSQL connection URL. Find it in the Supabase dashboard under <strong>Settings → Database → Connection string → URI</strong>.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db remote add supabase "postgres://postgres:[password]@db.[project-ref].supabase.co:5432/postgres" --default</span><br></span></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-import-the-existing-schema">Step 4: Import the Existing Schema<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#step-4-import-the-existing-schema" class="hash-link" aria-label="Direct link to Step 4: Import the Existing Schema" title="Direct link to Step 4: Import the Existing Schema" translate="no">​</a></h2>
<p>PostKit's <code>db import</code> command connects to your Supabase database, dumps the schema, and organizes it into PostKit's directory structure:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db import --url "postgres://postgres:[password]@db.[project-ref].supabase.co:5432/postgres"</span><br></span></code></pre></div></div>
<p>For projects with multiple schemas (e.g. <code>public</code> and a custom <code>app</code> schema):</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db import --url "postgres://..." --schemas "public,app"</span><br></span></code></pre></div></div>
<p>This automatically:</p>
<ul>
<li class="">Dumps all tables, views, functions, triggers, indexes, and constraints</li>
<li class="">Organizes files into <code>db/schema/public/tables/</code>, <code>views/</code>, <code>functions/</code>, etc.</li>
<li class="">Extracts roles and schemas into <code>db/infra/</code></li>
<li class="">Creates a baseline migration in <code>.postkit/db/migrations/</code></li>
<li class="">Updates <code>postkit.config.json</code> with the imported schema names</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5-review-the-generated-schema-files">Step 5: Review the Generated Schema Files<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#step-5-review-the-generated-schema-files" class="hash-link" aria-label="Direct link to Step 5: Review the Generated Schema Files" title="Direct link to Step 5: Review the Generated Schema Files" translate="no">​</a></h2>
<p>After import, your directory looks like:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">db/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── infra/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│   ├── roles.sql          # Any custom roles from Supabase</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">│   └── schemas.sql        # Schema definitions</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└── schema/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    └── public/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ├── tables/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        │   ├── 001_users.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        │   ├── 002_posts.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        │   └── ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ├── views/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ├── functions/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        └── grants/</span><br></span></code></pre></div></div>
<p>Take a few minutes to review. Supabase often creates helper functions and policies automatically — you'll want to understand what's there before making changes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="supabase-specific-objects-to-watch-for">Supabase-specific objects to watch for<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#supabase-specific-objects-to-watch-for" class="hash-link" aria-label="Direct link to Supabase-specific objects to watch for" title="Direct link to Supabase-specific objects to watch for" translate="no">​</a></h3>
<ul>
<li class=""><code>auth.*</code> schema — Supabase's internal auth tables. <strong>Do not touch these</strong> — they're managed by Supabase, not your migrations.</li>
<li class=""><code>storage.*</code> schema — same, managed by Supabase.</li>
<li class="">Row-level security (RLS) policies — pgschema captures these in <code>grants/</code> files.</li>
<li class=""><code>supabase_admin</code> role — Supabase-internal role. Ignore or exclude.</li>
</ul>
<p>If you only want to manage your own schemas (e.g. <code>public</code> and <code>app</code>) and leave <code>auth</code> and <code>storage</code> alone:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// postkit.config.json</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"db"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"schemas"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"public"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"app"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6-make-your-first-change-with-postkit">Step 6: Make Your First Change with PostKit<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#step-6-make-your-first-change-with-postkit" class="hash-link" aria-label="Direct link to Step 6: Make Your First Change with PostKit" title="Direct link to Step 6: Make Your First Change with PostKit" translate="no">​</a></h2>
<p>Now the normal workflow applies. Start a session:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db start</span><br></span></code></pre></div></div>
<p>Edit a schema file — for example, add a column:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- db/schema/public/tables/001_users.sql</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  id UUID </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> gen_random_uuid</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  email </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">UNIQUE</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  display_name </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">              </span><span class="token comment" style="color:#999988;font-style:italic">-- new column</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  created_at TIMESTAMPTZ </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NOW</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Preview the diff:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db plan</span><br></span></code></pre></div></div>
<p>PostKit shows you exactly what SQL will be generated — no surprises.</p>
<p>Apply locally to test:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db apply</span><br></span></code></pre></div></div>
<p>Commit and deploy:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db commit --message "add display_name to users"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db deploy</span><br></span></code></pre></div></div>
<p>PostKit runs a dry-run first (on a local clone of your Supabase DB), confirms the migration works, then deploys to the real database.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="common-supabase-migration-gotchas">Common Supabase Migration Gotchas<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#common-supabase-migration-gotchas" class="hash-link" aria-label="Direct link to Common Supabase Migration Gotchas" title="Direct link to Common Supabase Migration Gotchas" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="rls-policies-after-import">RLS policies after import<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#rls-policies-after-import" class="hash-link" aria-label="Direct link to RLS policies after import" title="Direct link to RLS policies after import" translate="no">​</a></h3>
<p>Supabase enables RLS on many tables. pgschema captures existing policies, but new policies you add to schema files are planned per-schema in isolation. Policies that reference roles from <code>auth.*</code> (like <code>auth.uid()</code>) are fine — pgschema recognizes these as external references.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="extensions">Extensions<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#extensions" class="hash-link" aria-label="Direct link to Extensions" title="Direct link to Extensions" translate="no">​</a></h3>
<p>Supabase pre-installs extensions like <code>uuid-ossp</code>, <code>pgcrypto</code>, and <code>pg_stat_statements</code>. These were captured by import in <code>db/infra/</code>. The <code>CREATE EXTENSION IF NOT EXISTS</code> form means re-running them is safe.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-public-schema-default">The <code>public</code> schema default<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#the-public-schema-default" class="hash-link" aria-label="Direct link to the-public-schema-default" title="Direct link to the-public-schema-default" translate="no">​</a></h3>
<p>Supabase's <code>public</code> schema has a broad default grant (<code>GRANT ALL ON SCHEMA public TO public</code>). After import, this appears in your <code>grants/</code> file. You can tighten or customize it.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-to-do-with-existing-supabase-migrations">What to Do With Existing Supabase Migrations<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#what-to-do-with-existing-supabase-migrations" class="hash-link" aria-label="Direct link to What to Do With Existing Supabase Migrations" title="Direct link to What to Do With Existing Supabase Migrations" translate="no">​</a></h2>
<p>If you have existing Supabase migration files (<code>.sql</code> files in <code>supabase/migrations/</code>), you don't need to port them. PostKit's <code>db import</code> creates a single baseline from the current database state — all prior history is collapsed into that baseline. Future changes go through PostKit's workflow.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://docs.postkitstack.com/blog/migrating-from-supabase-to-postkit#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<table><thead><tr><th>Step</th><th>Command</th></tr></thead><tbody><tr><td>Install PostKit</td><td><code>npm install -g @appritech/postkit</code></td></tr><tr><td>Init project</td><td><code>postkit init</code></td></tr><tr><td>Add Supabase remote</td><td><code>postkit db remote add supabase "postgres://..."</code></td></tr><tr><td>Import existing schema</td><td><code>postkit db import --url "postgres://..."</code></td></tr><tr><td>Start developing</td><td><code>postkit db start</code></td></tr><tr><td>Preview changes</td><td><code>postkit db plan</code></td></tr><tr><td>Apply locally</td><td><code>postkit db apply</code></td></tr><tr><td>Deploy to Supabase</td><td><code>postkit db deploy</code></td></tr></tbody></table>
<p>Your Supabase database is now under PostKit's session-based migration workflow. Schema changes are reviewed, tested locally, and deployed with a dry-run safety check before they ever touch production.</p>]]></content>
        <author>
            <name>PostKit Team</name>
            <uri>https://github.com/appritechnologies</uri>
        </author>
        <category label="migration" term="migration"/>
        <category label="supabase" term="supabase"/>
        <category label="postgres" term="postgres"/>
        <category label="workflow" term="workflow"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why We Clone the Database Before Every Migration]]></title>
        <id>https://docs.postkitstack.com/blog/why-session-based-migrations</id>
        <link href="https://docs.postkitstack.com/blog/why-session-based-migrations"/>
        <updated>2026-05-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Most database migration tools work the same way: write some SQL, run it against the database, hope for the best. PostKit takes a different approach — before any migration runs, we clone the production database to a local copy and work against that. It sounds like extra overhead. In practice, it catches a category of bugs that no amount of unit testing can find.]]></summary>
        <content type="html"><![CDATA[<p>Most database migration tools work the same way: write some SQL, run it against the database, hope for the best. PostKit takes a different approach — before any migration runs, we clone the production database to a local copy and work against that. It sounds like extra overhead. In practice, it catches a category of bugs that no amount of unit testing can find.</p>
<p><img decoding="async" loading="lazy" alt="Why We Clone the Database Before Every Migration" src="https://docs.postkitstack.com/assets/images/session-based-migrations-b4f468d7e94832db83a600642c98cbf5.jpg" width="1168" height="784" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-with-write-and-pray">The Problem with "Write and Pray"<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#the-problem-with-write-and-pray" class="hash-link" aria-label="Direct link to The Problem with &quot;Write and Pray&quot;" title="Direct link to The Problem with &quot;Write and Pray&quot;" translate="no">​</a></h2>
<p>Typical migration workflow:</p>
<ol>
<li class="">Write an <code>ALTER TABLE</code> statement</li>
<li class="">Run it against staging</li>
<li class="">If staging looks okay, run it against production</li>
</ol>
<p>The failure modes here are well-known:</p>
<ul>
<li class=""><strong>Missing index on a large table</strong>: migration takes 3 hours, locks the table, site goes down</li>
<li class=""><strong>Foreign key constraint violation</strong>: existing data fails the new constraint, migration rolls back</li>
<li class=""><strong>Wrong column type cast</strong>: <code>ALTER COLUMN foo TYPE integer USING foo::integer</code> fails because some rows have non-numeric values</li>
<li class=""><strong>Schema drift</strong>: staging has been modified manually over time, production has a different column order or default value</li>
</ul>
<p>These failures share a root cause: you didn't test the migration against a copy of the actual data.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-session-based-approach">The Session-Based Approach<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#the-session-based-approach" class="hash-link" aria-label="Direct link to The Session-Based Approach" title="Direct link to The Session-Based Approach" translate="no">​</a></h2>
<p>PostKit's model:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db start   →  clone production data to local DB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db plan    →  generate schema diff (pgschema)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db apply   →  apply migration to local clone</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db commit  →  lock in the migration files</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db deploy  →  dry-run on fresh clone, then deploy to production</span><br></span></code></pre></div></div>
<p>The key step is <code>start</code>. It uses <code>pg_dump</code> inside a version-matched Docker container to clone your remote database to a local PostgreSQL instance. This means:</p>
<ul>
<li class=""><strong>Your apply runs against real data</strong>, not an empty schema</li>
<li class=""><strong>Version mismatch is impossible</strong>: the container image is selected to match your remote's major version</li>
<li class=""><strong>No surprises on deploy</strong>: the migration has already run successfully on a copy of the exact database it will target</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-dry-run-step">The Dry-Run Step<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#the-dry-run-step" class="hash-link" aria-label="Direct link to The Dry-Run Step" title="Direct link to The Dry-Run Step" translate="no">​</a></h2>
<p>Even with local testing, we run one more safety check during deploy: a fresh clone of the target database is spun up, the migration is applied there first, and only if it succeeds does PostKit apply to the real target.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">deploy:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  1. Clone target DB to a new local container</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  2. Run the migration on that clone</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  3. If it fails → stop, report the error, touch nothing</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  4. If it succeeds → apply to the real target</span><br></span></code></pre></div></div>
<p>This catches the rare case where your local clone diverged from the target (e.g. someone else applied a manual change directly to staging).</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pgschema-diff-instead-of-write">pgschema: Diff Instead of Write<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#pgschema-diff-instead-of-write" class="hash-link" aria-label="Direct link to pgschema: Diff Instead of Write" title="Direct link to pgschema: Diff Instead of Write" translate="no">​</a></h2>
<p>PostKit uses <a href="https://pgschema.com/" target="_blank" rel="noopener noreferrer" class="">pgschema</a> to generate migration SQL. Rather than writing <code>ALTER TABLE</code> statements by hand, you declare the desired state in SQL files:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- db/schema/public/tables/users.sql</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  id    UUID </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> gen_random_uuid</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  email </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">UNIQUE</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  name  </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain">              </span><span class="token comment" style="color:#999988;font-style:italic">-- added this</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>postkit db plan</code> compares this file against the current database state and generates the minimal diff:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users </span><span class="token keyword" style="color:#00009f">ADD</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">COLUMN</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>You don't write the migration — you write the schema. The tool figures out what needs to change.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-workflow-prevents">What This Workflow Prevents<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#what-this-workflow-prevents" class="hash-link" aria-label="Direct link to What This Workflow Prevents" title="Direct link to What This Workflow Prevents" translate="no">​</a></h2>
<table><thead><tr><th>Problem</th><th>Traditional workflow</th><th>PostKit session workflow</th></tr></thead><tbody><tr><td>Large table lock</td><td>Discovered in production</td><td>Discovered on local clone with real data</td></tr><tr><td>FK constraint on existing data</td><td>May pass on empty staging</td><td>Fails on apply against real data</td></tr><tr><td>Wrong type cast</td><td>Silent data loss</td><td>Error during local apply</td></tr><tr><td>Schema drift between envs</td><td>Silent until deploy fails</td><td>Caught by dry-run before deploy</td></tr><tr><td>Human-written <code>ALTER TABLE</code> bugs</td><td>Typos reach production</td><td>pgschema generates the SQL</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-trade-off">The Trade-off<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#the-trade-off" class="hash-link" aria-label="Direct link to The Trade-off" title="Direct link to The Trade-off" translate="no">​</a></h2>
<p>The session approach costs you the time to clone the database at the start. For a 10GB database, that can be 2–5 minutes. For most teams this is a worthwhile investment — the alternative is discovering production issues after deployment.</p>
<p>For databases where a full clone is impractical (100GB+), you can use a representative subset or a pre-existing local PostgreSQL instance by setting <code>localDbUrl</code> in <code>postkit.secrets.json</code>. PostKit then skips the clone and connects directly.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="getting-started">Getting Started<a href="https://docs.postkitstack.com/blog/why-session-based-migrations#getting-started" class="hash-link" aria-label="Direct link to Getting Started" title="Direct link to Getting Started" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">npm install -g @appritech/postkit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit init</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db remote add prod "postgres://..." --default</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db start    # clone happens here</span><br></span></code></pre></div></div>
<p>From there, edit your schema files and run <code>postkit db plan</code> to see what changes.</p>]]></content>
        <author>
            <name>PostKit Team</name>
            <uri>https://github.com/appritechnologies</uri>
        </author>
        <category label="migrations" term="migrations"/>
        <category label="workflow" term="workflow"/>
        <category label="postgres" term="postgres"/>
        <category label="best-practices" term="best-practices"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Managing Multiple PostgreSQL Schemas Without the Pain]]></title>
        <id>https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas</id>
        <link href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas"/>
        <updated>2026-05-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[PostgreSQL schemas are namespaces inside a single database. Most applications start with just public and never think about it again. But as applications grow — multi-tenant SaaS, microservices sharing a database, strict separation between application and audit data — you eventually want multiple schemas. And then the tooling usually falls apart.]]></summary>
        <content type="html"><![CDATA[<p>PostgreSQL schemas are namespaces inside a single database. Most applications start with just <code>public</code> and never think about it again. But as applications grow — multi-tenant SaaS, microservices sharing a database, strict separation between application and audit data — you eventually want multiple schemas. And then the tooling usually falls apart.</p>
<p>PostKit handles multiple schemas natively, with a clear model for what goes where.</p>
<p><img decoding="async" loading="lazy" alt="Managing Multiple PostgreSQL Schemas" src="https://docs.postkitstack.com/assets/images/multi-schema-postgres-58059be139c367aaeadd315ea8a70225.jpg" width="1168" height="784" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-multiple-schemas">Why Multiple Schemas?<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#why-multiple-schemas" class="hash-link" aria-label="Direct link to Why Multiple Schemas?" title="Direct link to Why Multiple Schemas?" translate="no">​</a></h2>
<p>Common reasons teams reach for multiple schemas:</p>
<ul>
<li class=""><strong>Application vs. internal separation</strong>: <code>public</code> for user-facing tables, <code>app</code> for internal/operational tables, <code>audit</code> for immutable audit logs</li>
<li class=""><strong>Feature namespacing</strong>: each major domain gets its own schema to prevent table name collisions and enforce clear ownership</li>
<li class=""><strong>Permission isolation</strong>: different roles can be granted access to different schemas with a single <code>GRANT USAGE ON SCHEMA</code></li>
<li class=""><strong>Multi-tenant</strong>: per-tenant schemas (advanced use case, not covered here)</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-challenge-tools-that-think-in-one-schema">The Challenge: Tools That Think in One Schema<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#the-challenge-tools-that-think-in-one-schema" class="hash-link" aria-label="Direct link to The Challenge: Tools That Think in One Schema" title="Direct link to The Challenge: Tools That Think in One Schema" translate="no">​</a></h2>
<p>Most migration tools (Flyway, Liquibase, Prisma Migrate, plain dbmate) run a list of SQL files in order. They don't understand schema structure — they just execute. This works fine for <code>public</code>. It breaks when you have <code>public.users</code> and <code>app.orders</code> with a foreign key between them, because you have to manually manage the execution order across files.</p>
<p>PostKit's approach:</p>
<ul>
<li class=""><strong>Each schema is planned independently</strong> by pgschema (the diff engine)</li>
<li class=""><strong>Schemas execute in config order</strong> — dependencies go earlier in the array</li>
<li class=""><strong>Cross-schema SQL</strong> (FKs, views, triggers spanning two schemas) uses manual migrations applied after all schemas are set up</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="setup-adding-a-second-schema">Setup: Adding a Second Schema<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#setup-adding-a-second-schema" class="hash-link" aria-label="Direct link to Setup: Adding a Second Schema" title="Direct link to Setup: Adding a Second Schema" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-scaffold-the-directory">1. Scaffold the directory<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#1-scaffold-the-directory" class="hash-link" aria-label="Direct link to 1. Scaffold the directory" title="Direct link to 1. Scaffold the directory" translate="no">​</a></h3>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db schema add app</span><br></span></code></pre></div></div>
<p>This creates <code>db/schema/app/</code> with subdirectories for tables, views, functions, etc., and updates <code>postkit.config.json</code>:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"db"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"schemas"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"public"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"app"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"infraPath"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"db/infra"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"schemaPath"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"db/schema"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Array order matters — <code>public</code> runs before <code>app</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-add-the-schema-creation-to-infra">2. Add the schema creation to infra<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#2-add-the-schema-creation-to-infra" class="hash-link" aria-label="Direct link to 2. Add the schema creation to infra" title="Direct link to 2. Add the schema creation to infra" translate="no">​</a></h3>
<p>PostKit's infra step handles DB-level objects that pgschema doesn't manage:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- db/infra/002_schemas.sql</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">SCHEMA</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">IF</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">EXISTS</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-write-schema-files-for-the-new-schema">3. Write schema files for the new schema<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#3-write-schema-files-for-the-new-schema" class="hash-link" aria-label="Direct link to 3. Write schema files for the new schema" title="Direct link to 3. Write schema files for the new schema" translate="no">​</a></h3>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- db/schema/app/tables/orders.sql</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  id         UUID </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> gen_random_uuid</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  user_id    UUID </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">status</span><span class="token plain">     </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'pending'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  created_at TIMESTAMPTZ </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NOW</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Note: <code>user_id</code> is a plain UUID column here, not a foreign key to <code>public.users</code>. Cross-schema FKs require a different approach (below).</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-planning-works-with-multiple-schemas">How Planning Works with Multiple Schemas<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#how-planning-works-with-multiple-schemas" class="hash-link" aria-label="Direct link to How Planning Works with Multiple Schemas" title="Direct link to How Planning Works with Multiple Schemas" translate="no">​</a></h2>
<p>When you run <code>postkit db plan</code>, PostKit processes schemas in config order:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">1. Apply infra (CREATE SCHEMA app, CREATE ROLE ...) to local DB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2. Plan public schema:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   - pgschema compares db/schema/public/ against local DB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   - Generates plan_public.sql if changes exist</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   - Intermediate apply: runs plan_public.sql on local DB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3. Plan app schema:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   - pgschema compares db/schema/app/ against local DB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   - public schema objects are now present (from step 2)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   - Generates plan_app.sql if changes exist</span><br></span></code></pre></div></div>
<p>The intermediate apply in step 2 is what makes cross-schema resolution possible during the plan phase — <code>app</code> can reference tables from <code>public</code> via intra-session state, even though pgschema plans each schema in isolation.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cross-schema-foreign-keys">Cross-Schema Foreign Keys<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#cross-schema-foreign-keys" class="hash-link" aria-label="Direct link to Cross-Schema Foreign Keys" title="Direct link to Cross-Schema Foreign Keys" translate="no">​</a></h2>
<p>Here's where developers get confused. pgschema plans each schema in an isolated environment. If you write this in <code>db/schema/app/tables/orders.sql</code>:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- This will fail during plan:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">REFERENCES</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">-- ❌ public.users doesn't exist in pgschema's isolated env</span><br></span></code></pre></div></div>
<p>You'll get:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">ERROR: relation "public.users" does not exist</span><br></span></code></pre></div></div>
<p><strong>The correct approach</strong>: keep the column as a plain UUID in the schema file, then add the constraint as a manual migration.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db migration add_cross_schema_fks</span><br></span></code></pre></div></div>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- migrate:up</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">ADD</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">CONSTRAINT</span><span class="token plain"> fk_orders_user</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">FOREIGN</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">REFERENCES</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">-- migrate:down</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders </span><span class="token keyword" style="color:#00009f">DROP</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">CONSTRAINT</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">IF</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">EXISTS</span><span class="token plain"> fk_orders_user</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Manual migrations run via dbmate after all schemas are applied — at that point, <code>public.users</code> exists and the FK works.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-belongs-where">What Belongs Where<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#what-belongs-where" class="hash-link" aria-label="Direct link to What Belongs Where" title="Direct link to What Belongs Where" translate="no">​</a></h2>
<table><thead><tr><th>Object</th><th>Location</th><th>Managed by</th></tr></thead><tbody><tr><td>Tables, indexes, views within one schema</td><td><code>db/schema/&lt;name&gt;/</code></td><td>pgschema → dbmate</td></tr><tr><td>Roles, extensions, <code>CREATE SCHEMA</code></td><td><code>db/infra/</code></td><td>Infra step (psql)</td></tr><tr><td>Cross-schema FK constraints</td><td><code>postkit db migration</code></td><td>dbmate (manual)</td></tr><tr><td>Cross-schema views, functions, triggers</td><td><code>postkit db migration</code></td><td>dbmate (manual)</td></tr><tr><td>Seed data</td><td><code>db/schema/&lt;name&gt;/seeds/</code></td><td>Seeds step</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="importing-an-existing-multi-schema-database">Importing an Existing Multi-Schema Database<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#importing-an-existing-multi-schema-database" class="hash-link" aria-label="Direct link to Importing an Existing Multi-Schema Database" title="Direct link to Importing an Existing Multi-Schema Database" translate="no">​</a></h2>
<p>If you already have a multi-schema database, import everything at once:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db import --url "postgres://..." --schemas "public,app,audit"</span><br></span></code></pre></div></div>
<p>PostKit dumps all three schemas, organizes them into subdirectories, and creates a single baseline migration covering all schemas.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-result">The Result<a href="https://docs.postkitstack.com/blog/managing-multiple-postgres-schemas#the-result" class="hash-link" aria-label="Direct link to The Result" title="Direct link to The Result" translate="no">​</a></h2>
<p>Once set up, the workflow is identical to single-schema projects:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">postkit db start</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># edit db/schema/public/ or db/schema/app/ files</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db plan    # shows diffs per schema</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db apply</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db commit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db deploy</span><br></span></code></pre></div></div>
<p>Each schema's changes are shown separately in the plan output, so you can review <code>public</code> and <code>app</code> changes independently before applying.</p>]]></content>
        <author>
            <name>PostKit Team</name>
            <uri>https://github.com/appritechnologies</uri>
        </author>
        <category label="schemas" term="schemas"/>
        <category label="postgres" term="postgres"/>
        <category label="multi-tenant" term="multi-tenant"/>
        <category label="architecture" term="architecture"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Schema as Code vs Hand-Written Migrations: Why the Diff Approach Wins]]></title>
        <id>https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations</id>
        <link href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations"/>
        <updated>2026-05-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[There are two schools of thought on database migrations. The first says declare the desired schema state, let a tool figure out what SQL to generate. PostKit uses the second approach. Here's why.]]></summary>
        <content type="html"><![CDATA[<p>There are two schools of thought on database migrations. The first says: write every <code>ALTER TABLE</code> by hand, accumulate them as numbered files, and replay them in order. The second says: declare the desired schema state, let a tool figure out what SQL to generate. PostKit uses the second approach. Here's why.</p>
<p><img decoding="async" loading="lazy" alt="Schema as Code vs Hand-Written Migrations" src="https://docs.postkitstack.com/assets/images/schema-as-code-da9800edc20ee024d900b183bc66a486.png" width="2560" height="1440" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-traditional-approach">The Traditional Approach<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#the-traditional-approach" class="hash-link" aria-label="Direct link to The Traditional Approach" title="Direct link to The Traditional Approach" translate="no">​</a></h2>
<p>Tools like Flyway, Liquibase, and raw dbmate work like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">migrations/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── 001_create_users.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── 002_add_email_to_users.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── 003_create_posts.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── 004_add_index_on_posts_user_id.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└── 005_rename_posts_to_articles.sql</span><br></span></code></pre></div></div>
<p>To understand your current schema, you have to mentally replay all 200+ migrations. To change a column type, you write an <code>ALTER COLUMN</code>. To add an index, you write <code>CREATE INDEX</code>. Every migration is a manual, imperative instruction.</p>
<p>This model has real benefits — migrations are explicit, auditable, and easy to reason about individually. But it has a compounding cost: as the migration count grows, the cognitive overhead of understanding and maintaining the schema grows with it.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-declarative-approach">The Declarative Approach<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#the-declarative-approach" class="hash-link" aria-label="Direct link to The Declarative Approach" title="Direct link to The Declarative Approach" translate="no">​</a></h2>
<p>PostKit uses pgschema, a diff engine for PostgreSQL. Instead of writing migrations, you maintain SQL files that describe what your schema <em>should</em> look like:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- db/schema/public/tables/users.sql</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  id         UUID </span><span class="token keyword" style="color:#00009f">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">KEY</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> gen_random_uuid</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  email      </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">UNIQUE</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  display_name </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  created_at TIMESTAMPTZ </span><span class="token keyword" style="color:#00009f">DEFAULT</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NOW</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>When you want to add a column, you add it to the file. When you want to remove an index, you delete it from the file. The schema files are always the single source of truth for your current schema shape.</p>
<p><code>postkit db plan</code> compares those files against the actual database and generates the minimal migration:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">-- Generated by pgschema — you don't write this</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">ALTER</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">TABLE</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users </span><span class="token keyword" style="color:#00009f">ADD</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">COLUMN</span><span class="token plain"> display_name </span><span class="token keyword" style="color:#00009f">TEXT</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-you-dont-have-to-write">What You Don't Have to Write<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#what-you-dont-have-to-write" class="hash-link" aria-label="Direct link to What You Don't Have to Write" title="Direct link to What You Don't Have to Write" translate="no">​</a></h2>
<p>With pgschema generating the diffs, you stop writing:</p>
<ul>
<li class=""><code>ALTER TABLE ... ADD COLUMN</code></li>
<li class=""><code>ALTER TABLE ... DROP COLUMN</code></li>
<li class=""><code>CREATE INDEX</code> / <code>DROP INDEX</code></li>
<li class=""><code>ALTER TABLE ... ALTER COLUMN ... TYPE</code></li>
<li class=""><code>CREATE TABLE</code> for new tables</li>
<li class=""><code>DROP TABLE</code> for removed tables</li>
<li class="">Column default additions and removals</li>
<li class="">Constraint additions and removals (within a schema)</li>
</ul>
<p>These are the bread-and-butter of day-to-day schema work. Getting them for free removes a whole category of typos and missed steps.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-you-still-write-manually">What You Still Write Manually<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#what-you-still-write-manually" class="hash-link" aria-label="Direct link to What You Still Write Manually" title="Direct link to What You Still Write Manually" translate="no">​</a></h2>
<p>Some SQL has to be written as manual migrations because pgschema can't diff it:</p>
<ul>
<li class=""><code>CREATE ROLE</code> / <code>CREATE EXTENSION</code> / <code>CREATE SCHEMA</code> → use <code>db/infra/</code></li>
<li class="">Cross-schema foreign keys and views → use <code>postkit db migration</code></li>
<li class="">Data backfills (UPDATE, INSERT) → use <code>postkit db migration</code></li>
<li class="">One-off operations (<code>RENAME</code>, column type changes with custom casts) → use <code>postkit db migration</code></li>
</ul>
<p>See <a class="" href="https://docs.postkitstack.com/docs/modules/db/plan-limitations">Plan Command Limitations</a> for the full list.</p>
<p>The pattern: structural schema changes within a single schema → let pgschema generate it. Everything else → write a manual migration.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-schema-files-as-documentation">The Schema Files as Documentation<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#the-schema-files-as-documentation" class="hash-link" aria-label="Direct link to The Schema Files as Documentation" title="Direct link to The Schema Files as Documentation" translate="no">​</a></h2>
<p>A side effect of the declarative model: your <code>db/schema/</code> directory is always an accurate picture of your current database schema. No need to reconstruct it from 200 migration files. Junior developers joining the team can read <code>db/schema/public/tables/</code> and understand the data model immediately.</p>
<p>Compare to the traditional model where the "documentation" is the migration history — useful for understanding how you got here, not for understanding where you are now.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mixing-both-approaches">Mixing Both Approaches<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#mixing-both-approaches" class="hash-link" aria-label="Direct link to Mixing Both Approaches" title="Direct link to Mixing Both Approaches" translate="no">​</a></h2>
<p>PostKit doesn't force you to abandon explicit migrations. The <code>postkit db migration</code> command creates a manual migration file that runs alongside pgschema-generated ones. In a single session:</p>
<ol>
<li class="">Edit schema files (pgschema generates the diff on <code>plan</code>)</li>
<li class="">Add a manual migration for a data backfill</li>
<li class=""><code>postkit db apply</code> runs both in sequence: pgschema's SQL first, then the manual migrations</li>
</ol>
<p>This means you get the declarative model for structural changes and the imperative model for everything else — each where it's appropriate.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-workflow-at-a-glance">The Workflow at a Glance<a href="https://docs.postkitstack.com/blog/schema-as-code-vs-hand-written-migrations#the-workflow-at-a-glance" class="hash-link" aria-label="Direct link to The Workflow at a Glance" title="Direct link to The Workflow at a Glance" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># Edit db/schema/public/tables/users.sql — add a column</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Edit db/schema/public/indexes/ — add an index</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db plan</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># → Shows: ADD COLUMN display_name, CREATE INDEX idx_users_display_name</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db apply</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># → Runs the migration on your local clone</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db commit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db deploy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># → Dry-run, then deploy to production</span><br></span></code></pre></div></div>
<p>The migration files are generated, not authored. Your job is maintaining the schema declaration; PostKit's job is figuring out the SQL.</p>]]></content>
        <author>
            <name>PostKit Team</name>
            <uri>https://github.com/appritechnologies</uri>
        </author>
        <category label="migrations" term="migrations"/>
        <category label="pgschema" term="pgschema"/>
        <category label="postgres" term="postgres"/>
        <category label="developer-experience" term="developer-experience"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[From Prisma Migrate to PostKit: Taking Back Your SQL]]></title>
        <id>https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit</id>
        <link href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit"/>
        <updated>2026-05-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Prisma is an excellent ORM and Prisma Migrate is a good solution for teams that want to stay in TypeScript and never write SQL. But some teams reach a point where they want direct SQL control — complex views, functions, triggers, RLS policies, custom indexes. Prisma Migrate's model makes these awkward. PostKit is built for exactly that use case.]]></summary>
        <content type="html"><![CDATA[<p>Prisma is an excellent ORM and Prisma Migrate is a good solution for teams that want to stay in TypeScript and never write SQL. But some teams reach a point where they want direct SQL control — complex views, functions, triggers, RLS policies, custom indexes. Prisma Migrate's model makes these awkward. PostKit is built for exactly that use case.</p>
<p><img decoding="async" loading="lazy" alt="From Prisma Migrate to PostKit" src="https://docs.postkitstack.com/assets/images/prisma-to-postkit-59b5ebe855e1d67aab218ca815465f68.jpg" width="1168" height="784" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-prisma-migrate-stops-scaling">When Prisma Migrate Stops Scaling<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#when-prisma-migrate-stops-scaling" class="hash-link" aria-label="Direct link to When Prisma Migrate Stops Scaling" title="Direct link to When Prisma Migrate Stops Scaling" translate="no">​</a></h2>
<p>Prisma Migrate works by translating your <code>schema.prisma</code> model into SQL migrations. This is great until you need:</p>
<ul>
<li class=""><strong>PostgreSQL views</strong> — not supported natively in <code>schema.prisma</code></li>
<li class=""><strong>RLS policies</strong> — not expressible in Prisma schema</li>
<li class=""><strong>Custom functions and triggers</strong> — require raw SQL blocks in Prisma with <code>db.execute</code></li>
<li class=""><strong>Multi-schema projects</strong> — Prisma has limited multi-schema support and it varies by database connector</li>
<li class=""><strong>Complex index types</strong> — GIN, BRIN, partial indexes require <code>@@index</code> raw map or custom SQL</li>
</ul>
<p>Teams usually end up with a hybrid: Prisma Migrate for tables, a separate folder of raw SQL files for everything else. PostKit unifies these under one workflow.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-migration-looks-like">What the Migration Looks Like<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#what-the-migration-looks-like" class="hash-link" aria-label="Direct link to What the Migration Looks Like" title="Direct link to What the Migration Looks Like" translate="no">​</a></h2>
<p>The transition has two phases: getting your current schema into PostKit, then switching your ongoing workflow.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="phase-1-import-your-existing-database">Phase 1: Import your existing database<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#phase-1-import-your-existing-database" class="hash-link" aria-label="Direct link to Phase 1: Import your existing database" title="Direct link to Phase 1: Import your existing database" translate="no">​</a></h3>
<p>PostKit's <code>db import</code> command reads your current PostgreSQL database (not your <code>schema.prisma</code>) and generates schema files:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">npm install -g @appritech/postkit</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit init</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Add your database as a remote</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db remote add dev "postgres://..." --default</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Import the current schema</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db import --url "postgres://..."</span><br></span></code></pre></div></div>
<p>This creates <code>db/schema/public/</code> with all your current tables, views, functions, and constraints organized into subdirectories. Your Prisma-managed tables appear as plain SQL files — PostKit doesn't know or care how they were created.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="phase-2-stop-running-prisma-migrate">Phase 2: Stop running <code>prisma migrate</code><a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#phase-2-stop-running-prisma-migrate" class="hash-link" aria-label="Direct link to phase-2-stop-running-prisma-migrate" title="Direct link to phase-2-stop-running-prisma-migrate" translate="no">​</a></h3>
<p>From this point forward:</p>
<ul>
<li class="">Schema changes go in <code>db/schema/&lt;name&gt;/</code> files</li>
<li class=""><code>postkit db plan</code> generates the SQL diff</li>
<li class=""><code>postkit db apply</code> → <code>postkit db commit</code> → <code>postkit db deploy</code> replaces <code>prisma migrate dev</code> / <code>prisma migrate deploy</code></li>
</ul>
<p>You can keep using Prisma as your ORM (queries, type generation, <code>prisma generate</code>). Just stop using <code>prisma migrate</code>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="side-by-side-comparison">Side-by-Side Comparison<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#side-by-side-comparison" class="hash-link" aria-label="Direct link to Side-by-Side Comparison" title="Direct link to Side-by-Side Comparison" translate="no">​</a></h2>
<table><thead><tr><th>Task</th><th>Prisma Migrate</th><th>PostKit</th></tr></thead><tbody><tr><td>Add a column</td><td>Edit <code>schema.prisma</code>, run <code>prisma migrate dev</code></td><td>Edit the table SQL file, run <code>postkit db plan &amp;&amp; postkit db apply</code></td></tr><tr><td>Add an index</td><td>Edit <code>schema.prisma</code></td><td>Add to <code>db/schema/&lt;name&gt;/indexes/</code></td></tr><tr><td>Create a view</td><td>Raw <code>db.execute()</code> in a migration</td><td>File in <code>db/schema/&lt;name&gt;/views/</code></td></tr><tr><td>Add RLS policy</td><td>Custom SQL in migration</td><td>File in <code>db/schema/&lt;name&gt;/policies/</code> (pgschema-managed)</td></tr><tr><td>Create a function</td><td>Custom SQL migration</td><td>File in <code>db/schema/&lt;name&gt;/functions/</code></td></tr><tr><td>Deploy to production</td><td><code>prisma migrate deploy</code></td><td><code>postkit db deploy</code> (with dry-run)</td></tr><tr><td>Inspect current schema</td><td>Read <code>schema.prisma</code></td><td>Read <code>db/schema/&lt;name&gt;/tables/*.sql</code></td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="keeping-prisma-for-queries">Keeping Prisma for Queries<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#keeping-prisma-for-queries" class="hash-link" aria-label="Direct link to Keeping Prisma for Queries" title="Direct link to Keeping Prisma for Queries" translate="no">​</a></h2>
<p>PostKit manages your schema; Prisma manages your application queries. These don't conflict. Your workflow becomes:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># Schema change:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 1. Edit db/schema/public/tables/users.sql</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db plan</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">postkit db apply</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 2. Regenerate Prisma client to pick up the new column:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npx prisma db pull  # update schema.prisma from DB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npx prisma generate # regenerate client</span><br></span></code></pre></div></div>
<p><code>prisma db pull</code> reads the current database schema into <code>schema.prisma</code>, which you then commit alongside the PostKit schema file changes. PostKit owns the source of truth; Prisma reads it.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-about-existing-prisma-migrations">What About Existing Prisma Migrations?<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#what-about-existing-prisma-migrations" class="hash-link" aria-label="Direct link to What About Existing Prisma Migrations?" title="Direct link to What About Existing Prisma Migrations?" translate="no">​</a></h2>
<p>You don't need to port them. <code>postkit db import</code> creates a single baseline from the current database state — all prior migration history (Prisma or otherwise) is collapsed into that baseline. PostKit only cares about the current state and future changes.</p>
<p>Your existing <code>prisma/migrations/</code> folder can stay in your repo as historical reference or be removed — PostKit doesn't interact with it.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-practical-transition-checklist">The Practical Transition Checklist<a href="https://docs.postkitstack.com/blog/migrating-from-prisma-migrate-to-postkit#the-practical-transition-checklist" class="hash-link" aria-label="Direct link to The Practical Transition Checklist" title="Direct link to The Practical Transition Checklist" translate="no">​</a></h2>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Run <code>postkit init</code> in your project root</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Add your remote database: <code>postkit db remote add dev "postgres://..."</code></li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Run <code>postkit db import --url "postgres://..."</code> to generate <code>db/schema/</code></li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Review generated files — especially <code>db/infra/</code> for roles and extensions</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->Add <code>prisma/migrations/</code> to your "no longer needed" list (keep for history, stop running)</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->On schema changes: edit <code>db/schema/</code> → <code>postkit db plan</code> → <code>postkit db apply</code></li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->After schema changes: <code>npx prisma db pull &amp;&amp; npx prisma generate</code> to update the Prisma client</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->On deploy: <code>postkit db commit &amp;&amp; postkit db deploy</code></li>
</ul>
<hr>
<p>If you're running a pure SQL application (PostgREST, raw <code>pg</code> queries), skip the Prisma steps entirely — PostKit stands alone.</p>]]></content>
        <author>
            <name>PostKit Team</name>
            <uri>https://github.com/appritechnologies</uri>
        </author>
        <category label="migration" term="migration"/>
        <category label="prisma" term="prisma"/>
        <category label="postgres" term="postgres"/>
        <category label="workflow" term="workflow"/>
    </entry>
</feed>