<?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: Bhargav</title>
    <description>The latest articles on DEV Community by Bhargav (@itsbrgv).</description>
    <link>https://dev.to/itsbrgv</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%2F3903127%2Fff9c9a56-bc9c-474d-9db9-da32fc3e5cd2.png</url>
      <title>DEV Community: Bhargav</title>
      <link>https://dev.to/itsbrgv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itsbrgv"/>
    <language>en</language>
    <item>
      <title>Silent Schema Drift: How a text-to-jsonb cast broke production</title>
      <dc:creator>Bhargav</dc:creator>
      <pubDate>Tue, 05 May 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/itsbrgv/silent-schema-drift-how-a-text-to-jsonb-cast-broke-production-2fn1</link>
      <guid>https://dev.to/itsbrgv/silent-schema-drift-how-a-text-to-jsonb-cast-broke-production-2fn1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kpv5wst679x1t3z48gx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kpv5wst679x1t3z48gx.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
A Payload CMS seed script, a production Postgres database, and a schema that had quietly diverged for months. Here's what broke and how to fix it.&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; Payload's push mode ran against production, hit columns that had drifted from &lt;code&gt;text&lt;/code&gt; to &lt;code&gt;jsonb&lt;/code&gt; in the schema definition, and Postgres refused the automatic cast. Fix is &lt;code&gt;USING content::jsonb&lt;/code&gt; in the ALTER. Real fix is never running push against prod and writing explicit migrations.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The Error
&lt;/h2&gt;

&lt;p&gt;Running a seed script against my production Postgres database, every attempt to initialize Payload CMS failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;DrizzleQueryError: Failed query: ALTER TABLE &lt;span class="s2"&gt;"series"&lt;/span&gt; ALTER COLUMN &lt;span class="s2"&gt;"content"&lt;/span&gt; SET DATA TYPE jsonb&lt;span class="p"&gt;;&lt;/span&gt;
error: column &lt;span class="s2"&gt;"content"&lt;/span&gt; cannot be cast automatically to &lt;span class="nb"&gt;type &lt;/span&gt;jsonb
hint: You might need to specify &lt;span class="s2"&gt;"USING content::jsonb"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same error kept repeating for different tables [&lt;code&gt;series&lt;/code&gt;, &lt;code&gt;documents&lt;/code&gt;, &lt;code&gt;notebooks_blocks&lt;/code&gt;, &lt;code&gt;projects&lt;/code&gt;, &lt;code&gt;work_experience&lt;/code&gt;], each time a different column.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is jsonb?
&lt;/h2&gt;

&lt;p&gt;Postgres stores JSON in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;json&lt;/code&gt;&lt;/strong&gt;: stores the raw text string exactly as written. Fast writes, slow reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jsonb&lt;/code&gt;&lt;/strong&gt;: parses the JSON and stores it in a decomposed binary format. Slightly slower writes, faster reads, and supports indexing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both hold the same data. For application data, you almost always want &lt;code&gt;jsonb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;text&lt;/code&gt; is just a plain string. Postgres has no idea whether it contains JSON, it's just characters. There's no automatic conversion from &lt;code&gt;text&lt;/code&gt; to &lt;code&gt;jsonb&lt;/code&gt; because Postgres can't guarantee your strings are valid JSON without checking every row.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Schema Drift?
&lt;/h2&gt;

&lt;p&gt;Schema drift is when your application's schema definition (what the code expects the database to look like) and the actual live database diverge over time.&lt;/p&gt;

&lt;p&gt;In this case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The production database was created when Payload and Drizzle stored Lexical editor fields as &lt;code&gt;text&lt;/code&gt; columns.&lt;/li&gt;
&lt;li&gt;At some point, I updated the schema definition in code to use &lt;code&gt;jsonb&lt;/code&gt; for those fields.&lt;/li&gt;
&lt;li&gt;The production database was never migrated to match.&lt;/li&gt;
&lt;li&gt;The application kept working because Postgres is flexible enough to return &lt;code&gt;text&lt;/code&gt; values to the application layer, which then parses them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The drift was silent. The site ran fine, no errors in production logs, nothing to indicate a problem, until I added a few collections to Payload and pushed the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Surfaced Now
&lt;/h2&gt;

&lt;p&gt;Payload CMS v3 with the Drizzle adapter has two modes for keeping the DB in sync:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Push mode&lt;/strong&gt; (&lt;code&gt;pushDevSchema&lt;/code&gt;): on startup, Drizzle compares its schema definition against the live DB and tries to &lt;code&gt;ALTER&lt;/code&gt; any mismatched columns. Default in development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration mode&lt;/strong&gt;: you write explicit SQL migration files and run them with &lt;code&gt;payload migrate&lt;/code&gt;. Use this in production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running the seed script (&lt;code&gt;payload run scripts/seed-resume.ts&lt;/code&gt;) called &lt;code&gt;getPayload()&lt;/code&gt;, which triggered &lt;code&gt;pushDevSchema&lt;/code&gt;. That kicked off the ALTER attempts and is where they failed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;payload migrate&lt;/code&gt; had run fine earlier and reported &lt;code&gt;Done.&lt;/code&gt; The migration only added new tables. It never touched the existing &lt;code&gt;content&lt;/code&gt; columns because I never wrote a migration for them. Drizzle push, on the other hand, noticed all schema drift and tried to fix everything at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Postgres Refuses the Automatic Cast
&lt;/h2&gt;

&lt;p&gt;When you run:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres needs to convert every existing row from &lt;code&gt;text&lt;/code&gt; to &lt;code&gt;jsonb&lt;/code&gt;, but it can't confirm your text values are valid JSON. If even one row contains plain text, parsing it as JSON would fail. You have to tell it explicitly to attempt the cast.&lt;/p&gt;

&lt;p&gt;The fix is the &lt;code&gt;USING&lt;/code&gt; clause:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This casts every value using the &lt;code&gt;::jsonb&lt;/code&gt; operator. If all values are valid JSON, it succeeds. If any aren't, it fails at the specific bad row — which is useful, because it forces you to confront bad data directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to Fix Schema Drift
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before any of this: take a backup.&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_dump &lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.sql
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Schema-altering operations are not forgiving. Do this first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Option 1: USING cast
&lt;/h3&gt;

&lt;p&gt;For each affected column, run the ALTER with an explicit cast:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"series"&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"content"&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when you're confident all existing values are valid JSON.&lt;/p&gt;

&lt;p&gt;If a column has a DEFAULT value, drop it first:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"documents"&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"parsed_content"&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&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="nv"&gt;"documents"&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"parsed_content"&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;parsed_content&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2: Null out the bad rows first
&lt;/h3&gt;

&lt;p&gt;If some rows contain invalid JSON:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;notebooks_blocks&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'{%'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'[%'&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;notebooks_blocks&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when you don't need the non-JSON rows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;LIKE '{%'&lt;/code&gt; filter is a heuristic. A valid JSON string like &lt;code&gt;"hello"&lt;/code&gt; starts with &lt;code&gt;"&lt;/code&gt;, not &lt;code&gt;{&lt;/code&gt; or &lt;code&gt;[&lt;/code&gt;. For stricter filtering, use &lt;code&gt;content ~ '^[\[{"]'&lt;/code&gt; or reach for &lt;code&gt;jsonb_typeof&lt;/code&gt; after a try-cast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Option 3: Skip the column for now
&lt;/h3&gt;

&lt;p&gt;If rows contain real data you can't afford to lose, leave the column as &lt;code&gt;text&lt;/code&gt; and come back when you have a migration plan for the content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 4: Write a proper migration
&lt;/h3&gt;

&lt;p&gt;The right long-term fix is a hand-written migration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// migrations/20260427_fix_jsonb_drift.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MigrateUpArgs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@payloadcms/db-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@payloadcms/db-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;up&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MigrateUpArgs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    ALTER TABLE "series" ALTER COLUMN "content" TYPE jsonb USING content::jsonb;
    ALTER TABLE "projects" ALTER COLUMN "content" TYPE jsonb USING content::jsonb;
    ALTER TABLE "work_experience" ALTER COLUMN "content" TYPE jsonb USING content::jsonb;
  `&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env.local node_modules/payload/bin.js migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way the fix lives in version control, is reproducible, and is documented alongside the code that caused the drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disable Push Mode in Production
&lt;/h2&gt;

&lt;p&gt;The root cause of this surfacing at all was Drizzle push running against a production database. Push mode is a development convenience and not safe for prod.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;payload.config.ts&lt;/code&gt;, disable it outside development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;postgresAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, schema changes go through &lt;code&gt;payload migrate&lt;/code&gt; with explicit, reviewed migration files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;The database and code can diverge for months while the application adapts, types get coerced, and nothing breaks visibly. When something finally tries to reconcile the two, a push, a migration tool, a new ORM version, it all surfaces at once.&lt;/p&gt;

&lt;p&gt;Signs you might have schema drift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your DB was created a long time ago and has never been formally migrated&lt;/li&gt;
&lt;li&gt;You've upgraded your ORM or database adapter&lt;/li&gt;
&lt;li&gt;You're running push mode on a database that predates the current schema definition&lt;/li&gt;
&lt;li&gt;Column types in the DB don't match what your application code declares&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How to stay ahead of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use migration files for schema changes, even small ones&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;payload migrate:status&lt;/code&gt; to see if anything is pending before you touch anything&lt;/li&gt;
&lt;li&gt;Never run push mode against a production database&lt;/li&gt;
&lt;li&gt;Take a backup (&lt;code&gt;pg_dump&lt;/code&gt;) before any schema-altering operation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://payloadcms.com/docs/database/migrations" rel="noopener noreferrer"&gt;Payload CMS — Migrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://payloadcms.com/docs/database/migrations#migration-status" rel="noopener noreferrer"&gt;Payload CMS — &lt;code&gt;migrate:status&lt;/code&gt; command&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://orm.drizzle.team/docs/migrations" rel="noopener noreferrer"&gt;Drizzle ORM — Migrations vs Push&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;PostgreSQL — ALTER TABLE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/datatype-json.html" rel="noopener noreferrer"&gt;PostgreSQL — JSON Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.postgresql.org/docs/current/functions-json.html" rel="noopener noreferrer"&gt;PostgreSQL — JSON Functions and Operators&lt;/a&gt; — specifically &lt;code&gt;jsonb_typeof&lt;/code&gt;, &lt;code&gt;jsonb_valid&lt;/code&gt; (Postgres 16+)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgressql</category>
      <category>payloadcms</category>
      <category>typescript</category>
      <category>json</category>
    </item>
  </channel>
</rss>
