<?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: Dobrin Dimchev</title>
    <description>The latest articles on DEV Community by Dobrin Dimchev (@dobrinyonkov).</description>
    <link>https://dev.to/dobrinyonkov</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%2F518501%2F739d8f72-8ba1-4e44-8a88-ba77502f645b.jpeg</url>
      <title>DEV Community: Dobrin Dimchev</title>
      <link>https://dev.to/dobrinyonkov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dobrinyonkov"/>
    <language>en</language>
    <item>
      <title>Your PR Preview Is Talking to Your Production Database</title>
      <dc:creator>Dobrin Dimchev</dc:creator>
      <pubDate>Tue, 14 Apr 2026 03:30:21 +0000</pubDate>
      <link>https://dev.to/dobrinyonkov/your-pr-preview-is-talking-to-your-production-database-4544</link>
      <guid>https://dev.to/dobrinyonkov/your-pr-preview-is-talking-to-your-production-database-4544</guid>
      <description>&lt;p&gt;You enabled preview deployments on Cloudflare. You opened a PR. The preview built, deployed, and is now reading and writing your production D1 database. Every user action on that preview URL hits prod data. And the moment you add migrations to your deploy command, those run against prod too.&lt;/p&gt;

&lt;p&gt;That's not a hypothetical. That's the default behavior.&lt;/p&gt;

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

&lt;p&gt;Cloudflare Workers preview deployments use the &lt;strong&gt;top-level bindings&lt;/strong&gt; in your &lt;code&gt;wrangler.jsonc&lt;/code&gt;. There is no magic separation. If your D1 &lt;code&gt;database_id&lt;/code&gt; points to production, every preview deployment — including the ones triggered by your half-finished feature branch — is bound to production. Reads, writes, all of it.&lt;/p&gt;

&lt;p&gt;The default non-production deploy command (&lt;code&gt;npx wrangler versions upload&lt;/code&gt;) won't run migrations on its own. But the preview Worker still connects to your prod database at runtime. And once you customize the deploy command to include migrations (which you will, because you need schema changes to actually work in previews), those migrations run against prod.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;preview_database_id&lt;/code&gt; field? That's only for &lt;code&gt;wrangler dev&lt;/code&gt; locally. It does nothing for deployed previews.&lt;/p&gt;

&lt;p&gt;There's another angle to this. If you're using AI coding agents that open PRs autonomously — background agents, CI bots, whatever — you get a preview deployment URL but it's either pointing at prod or the schema changes the agent made simply don't work because no migration ran. You can't actually verify what the agent built without a working, isolated database behind the preview. This makes the staging environment not just a safety measure, but a prerequisite for any kind of automated PR workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Wrangler Environments
&lt;/h2&gt;

&lt;p&gt;Wrangler supports &lt;a href="https://developers.cloudflare.com/workers/wrangler/environments/" rel="noopener noreferrer"&gt;environments&lt;/a&gt; — named configurations that deploy as separate Workers with their own bindings. You create a &lt;code&gt;staging&lt;/code&gt; environment, point it at a separate D1 database, and tell Cloudflare to use it for non-production deploys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create staging resources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler d1 create my-app-staging
npx wrangler kv namespace create KV_STAGING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the returned IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add &lt;code&gt;env.staging&lt;/code&gt; to your wrangler config
&lt;/h3&gt;

&lt;p&gt;Here's the critical part. Bindings in Cloudflare are &lt;strong&gt;non-inheritable&lt;/strong&gt;. You can't just override the D1 binding — you must redeclare every single binding inside &lt;code&gt;env.staging&lt;/code&gt;. If you forget one, that binding simply won't exist in your staging Worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"d1_databases"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"database_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"database_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aaaa-bbbb-cccc-prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"migrations_dir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./drizzle/migrations"&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;span class="nl"&gt;"kv_namespaces"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aaaa-bbbb-cccc-prod-kv"&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;span class="nl"&gt;"r2_buckets"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"R2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bucket_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&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;span class="nl"&gt;"env"&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;"staging"&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;"d1_databases"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"database_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app-staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"database_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxxx-yyyy-zzzz-staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"migrations_dir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./drizzle/migrations"&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;span class="nl"&gt;"kv_namespaces"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxxx-yyyy-zzzz-staging-kv"&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;span class="nl"&gt;"r2_buckets"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"R2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"bucket_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&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;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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same binding names, different resource IDs. Your application code doesn't change at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add staging scripts
&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;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm build &amp;amp;&amp;amp; wrangler d1 migrations apply DB --remote &amp;amp;&amp;amp; wrangler deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy:staging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLOUDFLARE_ENV=staging pnpm build &amp;amp;&amp;amp; npx wrangler d1 migrations apply DB --remote --env staging &amp;amp;&amp;amp; npx wrangler deploy --env staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:migrate:staging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx wrangler d1 migrations apply DB --remote --env staging"&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;
  
  
  Step 4: Apply initial migrations to staging
&lt;/h3&gt;

&lt;p&gt;Your staging database is empty. Seed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler d1 migrations apply DB &lt;span class="nt"&gt;--remote&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs every migration file in your &lt;code&gt;migrations_dir&lt;/code&gt; against the staging D1. You only need to do this once — after that, the deploy command handles it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Configure the Cloudflare dashboard
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Workers &amp;amp; Pages &amp;gt; your project &amp;gt; Settings &amp;gt; Build&lt;/strong&gt; and set the &lt;strong&gt;Non-production branch deploy command&lt;/strong&gt; to:&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="nv"&gt;CLOUDFLARE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging pnpm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx wrangler d1 migrations apply DB &lt;span class="nt"&gt;--remote&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; staging &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx wrangler versions upload &lt;span class="nt"&gt;--env&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. Every PR push now builds with staging bindings, migrates the staging D1, and uploads an isolated preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;CLOUDFLARE_ENV&lt;/code&gt; Gotcha
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;code&gt;@cloudflare/vite-plugin&lt;/code&gt; (you probably are if you're on React Router v7, Remix, or Astro with Cloudflare), environment selection happens at &lt;strong&gt;build time&lt;/strong&gt; via the &lt;code&gt;CLOUDFLARE_ENV&lt;/code&gt; environment variable — not the &lt;code&gt;--env&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CLOUDFLARE_ENV=staging pnpm build&lt;/code&gt; — builds with &lt;code&gt;env.staging&lt;/code&gt; bindings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npx wrangler deploy --env staging&lt;/code&gt; — deploys to the staging Worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need both. &lt;code&gt;CLOUDFLARE_ENV&lt;/code&gt; for the build step, &lt;code&gt;--env&lt;/code&gt; for the wrangler commands. Miss the first one and your build bakes in production bindings. Miss the second and wrangler deploys to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Non-Inheritable Trap
&lt;/h2&gt;

&lt;p&gt;This will bite you exactly once: you add a new KV namespace to your top-level config, deploy to production, everything works. Then a PR preview fails because the binding doesn't exist. You forgot to add it to &lt;code&gt;env.staging&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every time you add or change a binding at the top level, mirror it in &lt;code&gt;env.staging&lt;/code&gt;. There is no inheritance. There is no fallback. The staging Worker only sees what you explicitly declare.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's It
&lt;/h2&gt;

&lt;p&gt;Five steps. One new D1 database, one config block, one dashboard field. Your PR previews now have their own database, your migrations run in isolation, and you can sleep at night knowing a feature branch won't &lt;code&gt;DROP TABLE&lt;/code&gt; your users.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>d1</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
