<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Faouzane BATIGA</title>
    <description>The latest articles on DEV Community by Faouzane BATIGA (@faouzane_batiga_d9d321705).</description>
    <link>https://dev.to/faouzane_batiga_d9d321705</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3357701%2Fd463276b-8892-40a1-99ec-858301db534a.png</url>
      <title>DEV Community: Faouzane BATIGA</title>
      <link>https://dev.to/faouzane_batiga_d9d321705</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faouzane_batiga_d9d321705"/>
    <language>en</language>
    <item>
      <title>How to Keep Your Entire Supabase Database (policies, functions, triggers, cron) synced across envs</title>
      <dc:creator>Faouzane BATIGA</dc:creator>
      <pubDate>Tue, 15 Jul 2025 19:51:57 +0000</pubDate>
      <link>https://dev.to/faouzane_batiga_d9d321705/how-to-keep-your-entire-supabase-database-state-in-your-repository-3468</link>
      <guid>https://dev.to/faouzane_batiga_d9d321705/how-to-keep-your-entire-supabase-database-state-in-your-repository-3468</guid>
      <description>&lt;p&gt;Following a conversation on the Supabase reddit i felt like i should share my setup to help some people.&lt;/p&gt;

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

&lt;p&gt;I was constantly running into issues where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RLS UI is annoying&lt;/strong&gt; - managing policies through Supabase dashboard is clunky and error-prone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLS deployment bugs&lt;/strong&gt; - policies weren't being deployed as part of go-live, or were named differently from what they actually do&lt;/li&gt;
&lt;li&gt;My local database was different from staging&lt;/li&gt;
&lt;li&gt;Staging was different from production&lt;/li&gt;
&lt;li&gt;Database functions, triggers, and policies were getting lost&lt;/li&gt;
&lt;li&gt;Cron jobs would disappear after deployments&lt;/li&gt;
&lt;li&gt;Team members had inconsistent database states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row Level Security policies would drift&lt;/strong&gt; - manual changes in Supabase dashboard weren't in code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Create Your Own Migration Runner?
&lt;/h2&gt;

&lt;p&gt;You might be thinking: "Supabase has its own migration system, why reinvent the wheel?"&lt;/p&gt;

&lt;p&gt;Here's why I built my own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I was tired of needing to diff the database every time&lt;/strong&gt; I needed to create a table or add a column&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'm fluent in SQL&lt;/strong&gt;, so I don't want to use the Supabase UI for database changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase migration system kept breaking&lt;/strong&gt; and often required resetting the database state (#annoying #shittyDx)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Supabase migration system is great for simple use cases, but when you have complex database state (functions, triggers, policies, cron jobs) and need reliable deployments, you need something more robust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Database State as Code
&lt;/h2&gt;

&lt;p&gt;I created a system that keeps your &lt;strong&gt;entire database state&lt;/strong&gt; in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supabase/
├── run.js                 # Migration runner (the magic)
├── db-migrations/         # Schema changes
├── db-functions/          # PostgreSQL functions
├── policies.sql           # Row Level Security
├── triggers.sql           # Database triggers
└── cron.sql              # Scheduled jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  1. The Migration Runner (&lt;code&gt;run.js&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the core of the system. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tracks which migrations have been applied in a &lt;code&gt;supabase_migrations&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;Compares local files with applied migrations&lt;/li&gt;
&lt;li&gt;Runs pending migrations in order&lt;/li&gt;
&lt;li&gt;Handles rollbacks on errors&lt;/li&gt;
&lt;li&gt;Manages functions, triggers, policies, and cron jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Package.json Commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"migrate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node supabase/run.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sync:db"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node supabase/run.js --db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sync:policies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node supabase/run.js --policies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"create:migration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn migration --create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"create:function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn migration --create --func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"list:migrations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn migration --list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"revert:migration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn migration --revert"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Complete CLI Reference
&lt;/h3&gt;

&lt;p&gt;The migration runner provides comprehensive CLI options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run all pending migrations&lt;/span&gt;
yarn migrate

&lt;span class="c"&gt;# Run a specific migration&lt;/span&gt;
yarn migration &amp;lt;migration-name&amp;gt;

&lt;span class="c"&gt;# Skip a migration permanently&lt;/span&gt;
yarn migration &amp;lt;migration-name&amp;gt; &lt;span class="nt"&gt;--skip&lt;/span&gt;

&lt;span class="c"&gt;# Revert an already played migration&lt;/span&gt;
yarn migration &amp;lt;migration-name&amp;gt; &lt;span class="nt"&gt;--revert&lt;/span&gt;

&lt;span class="c"&gt;# List all migrations (applied and pending)&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--list&lt;/span&gt;

&lt;span class="c"&gt;# Create a new migration file&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--create&lt;/span&gt; &amp;lt;name&amp;gt;

&lt;span class="c"&gt;# Create a new function file&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--create&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--func&lt;/span&gt;

&lt;span class="c"&gt;# Run in debug mode&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--debug&lt;/span&gt;

&lt;span class="c"&gt;# Sync database policies only&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--policies&lt;/span&gt;

&lt;span class="c"&gt;# Sync everything (functions, triggers, policies, cron)&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--db&lt;/span&gt;

&lt;span class="c"&gt;# Run without transactions (for statements that can't run in transactions)&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--no-transaction&lt;/span&gt;

&lt;span class="c"&gt;# Show help&lt;/span&gt;
yarn migration &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. File Organization
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Migrations&lt;/strong&gt; (&lt;code&gt;db-migrations/&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 2024-01-01_00_00_00_000Z-initial-schema.sql&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Functions&lt;/strong&gt; (&lt;code&gt;db-functions/&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 2024-01-01_00_00_00_000Z-get-user-posts.sql&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;get_user_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_uuid&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Policies&lt;/strong&gt; (&lt;code&gt;policies.sql&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- CRITICAL: This function must be at the top&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;supabase_migrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop_policies_in_transaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can view own profile"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Triggers&lt;/strong&gt; (&lt;code&gt;triggers.sql&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;update_updated_at_column&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="s1"&gt;'plpgsql'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cron Jobs&lt;/strong&gt; (&lt;code&gt;cron.sql&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'cleanup-old-data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'0 2 * * *'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'DELETE FROM posts WHERE created_at &amp;lt; NOW() - INTERVAL &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;1 year&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚨 Critical: Policy Synchronization
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;drop_policies_in_transaction()&lt;/code&gt; function is &lt;strong&gt;essential&lt;/strong&gt; for keeping RLS policies in sync:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This function drops all existing policies before applying new ones&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;
&lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;supabase_migrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop_policies_in_transaction&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="n"&gt;policy_record&lt;/span&gt; &lt;span class="n"&gt;RECORD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="c1"&gt;-- Create a temporary table to store the policies&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMPORARY&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;temp_policies&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Loop through the policies and drop each one&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;policy_record&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temp_policies&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt; &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="s1"&gt;'DROP POLICY IF EXISTS "'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;policy_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policyname&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'" ON "'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;policy_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="c1"&gt;-- Log the exception or take appropriate action&lt;/span&gt;
&lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'Error dropping policy % on table %: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;policy_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;policy_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;SQLERRM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Drop the temporary table&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;temp_policies&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is crucial:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Policies can be created manually in Supabase dashboard&lt;/li&gt;
&lt;li&gt;These manual policies won't be in your code&lt;/li&gt;
&lt;li&gt;Without dropping them first, you'll have &lt;strong&gt;orphaned policies&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The function ensures only policies defined in your code exist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevents security drift&lt;/strong&gt; - your RLS rules are always exactly what you've defined&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CI/CD Integration
&lt;/h2&gt;

&lt;p&gt;Here's how to integrate it into your GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy and migrate&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DATABASE_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;yarn deploy&lt;/span&gt;
    &lt;span class="s"&gt;yarn migrate&lt;/span&gt;
    &lt;span class="s"&gt;yarn sync:db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy your app&lt;/strong&gt; (Supabase functions, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run migrations&lt;/strong&gt; (&lt;code&gt;yarn migrate&lt;/code&gt;) - applies schema changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync database state&lt;/strong&gt; (&lt;code&gt;yarn sync:db&lt;/code&gt;) - applies functions, triggers, policies, cron&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;p&gt;✅ &lt;strong&gt;Consistent environments&lt;/strong&gt; - Everyone has the same database state&lt;br&gt;
✅ &lt;strong&gt;Version controlled&lt;/strong&gt; - All changes are tracked in git&lt;br&gt;
✅ &lt;strong&gt;Rollback support&lt;/strong&gt; - Can revert specific migrations&lt;br&gt;
✅ &lt;strong&gt;Team collaboration&lt;/strong&gt; - No more "works on my machine"&lt;br&gt;
✅ &lt;strong&gt;CI/CD ready&lt;/strong&gt; - Automated database deployments&lt;br&gt;
✅ &lt;strong&gt;Comprehensive&lt;/strong&gt; - Functions, triggers, policies, cron jobs included&lt;br&gt;
✅ &lt;strong&gt;Policy synchronization&lt;/strong&gt; - No more orphaned RLS policies&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Copy the structure&lt;/strong&gt; from the example below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up your database connection&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgresql://user:password@localhost:5432/dbname"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run your first migration&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   yarn migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example Repository
&lt;/h2&gt;

&lt;p&gt;I've created a minimal example showing the complete setup: &lt;a href="https://github.com/enyosolutions/supabase-synced-database-example/blob/master/README.md" rel="noopener noreferrer"&gt;database-state-example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key files are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;package.json&lt;/code&gt; - All the CLI commands&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase/run.js&lt;/code&gt; - The migration runner (this is the magic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase/db-functions/drop-policies-function.sql&lt;/code&gt; - Critical for policy sync&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;services/postgres.js&lt;/code&gt; the service in charge of connecting the db.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Traditional migration systems only handle schema changes. This approach treats your &lt;strong&gt;entire database state&lt;/strong&gt; as code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema&lt;/strong&gt; → &lt;code&gt;db-migrations/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functions&lt;/strong&gt; → &lt;code&gt;db-functions/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; → &lt;code&gt;policies.sql&lt;/code&gt; (with automatic cleanup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt; → &lt;code&gt;triggers.sql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt; → &lt;code&gt;cron.sql&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is version controlled, everything is applied consistently, and everything can be rolled back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use timestamps&lt;/strong&gt; in migration names for ordering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep functions separate&lt;/strong&gt; from migrations for better organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test locally first&lt;/strong&gt; before pushing to staging/prod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the &lt;code&gt;--no-transaction&lt;/code&gt; flag&lt;/strong&gt; for migrations that can't run in transactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;yarn migration --list&lt;/code&gt;&lt;/strong&gt; to see what's pending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always include &lt;code&gt;drop_policies_in_transaction()&lt;/code&gt;&lt;/strong&gt; at the top of policies.sql&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach has completely eliminated database drift issues for my team. No more "it works on my machine" - everyone's database is identical, including security policies.&lt;/p&gt;

&lt;p&gt;What do you think? Anyone else using a similar approach?&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
