Why your deploy preview can delete Netlify Blobs data
getStore() is site-scoped and shared across every deploy context, so preview code writes to production. Here's the blast radius and the one-line fix.
Key takeaways
getStore(name) is site-scoped, which means it’s shared across every deploy context. Code running on a Deploy Preview reads, overwrites, and deletes the exact same data your production site serves. The fix is getDeployStore(name), which scopes the store to a single deploy, or a store name that includes the deploy context. If you’ve been seeding test data or running destructive tests from a preview, that data went to production.
- One store name, one store. There’s no per-context namespace on a site-scoped store.
getDeployStore()is isolated from production and cleaned up when the deploy is deleted — not immediately when a newer deploy publishes over it, but when the deploy record itself is deleted.deleteAll()from a preview wipes the production store. No confirmation, no context check.- A test that writes to a fixed key overwrites the live value at that key. This is the quiet version, and the one that’s hardest to notice.
- Netlify Dev won’t reproduce this. Local development uses a sandboxed store and can’t reach production data, so the problem only appears once your branch is deployed.
The bug you don’t find in review
You added a feature that stores uploads in Blobs. You wrote a test that seeds a few records and clears them afterward. It passes locally. You open a pull request, the Deploy Preview builds, the test suite runs against the preview, everything’s green.
Meanwhile the records your production site was serving are gone.
Nothing in that sequence looks wrong. The mistake is one call, and it’s the call almost everyone writes first because it’s the shorter one. Here’s what’s actually happening and what to write instead.
When this applies to you
This is your situation if any of these are true:
- You call
getStore(...)anywhere in code that runs on a Deploy Preview or branch deploy - You seed, fixture, or clean up blob data from tests that run against a deployed preview
- You call
deleteAll()outside of production code paths - You use a fixed, predictable key (
"latest","cache","config") rather than a per-record ID
This is not your situation if you only ever call getDeployStore(...), or if all your blob writes happen in production code paths against per-record keys and you never run destructive operations from a preview. It’s also not your situation locally: Netlify Dev’s sandboxed store can’t touch production, which is precisely why this survives local testing.
If you’re choosing a storage primitive rather than debugging one, start at How to store files and objects with Netlify Blobs instead.
What’s actually happening
Blobs gives you two ways to open a store, and they differ in scope rather than in API:
import { getStore, getDeployStore } from "@netlify/blobs";
const siteStore = getStore("file-uploads"); // site-scoped: ALL contexts share thisconst deployStore = getDeployStore("file-uploads"); // this deploy onlygetStore("file-uploads") resolves to one store for the whole site. Production resolves it to that store. Your Deploy Preview resolves it to that same store. So does every branch deploy and every agent preview. The store persists across deploys, which is what you want for production data and exactly what makes preview writes dangerous.
getDeployStore("file-uploads") resolves to a store scoped to the deploy that’s running. Production’s copy and your preview’s copy are different stores that happen to share a name.
The blast radius
Ordered roughly by how bad it is versus how likely you are to notice:
| What preview code does | What happens to production |
|---|---|
store.deleteAll() | The entire production store is deleted. deletedBlobs reports the count. |
store.delete(key) | That production record is gone |
store.set(key, testValue) on a fixed key | The production value at that key is replaced. Quietest and most common. |
store.set(newKey, testValue) | Test data accumulates in the production store |
store.get(key) | Reads real user data into your preview environment |
The third row is the one worth staring at. A test that writes store.set("config", {...}) doesn’t error, doesn’t warn, and doesn’t leave an obvious trace. It just replaces whatever production had at "config" with your fixture. If nothing reads that key for a while, you find out later.
Note also that onlyIfNew and onlyIfMatch don’t save you here. They’re concurrency guards, not context guards. onlyIfNew will happily refuse to overwrite, but that means your test now behaves differently against a populated production store than it did against an empty local one.
The fix
For anything throwaway, per-deploy, or test-related, use the deploy store:
import { getDeployStore } from "@netlify/blobs";
const uploads = getDeployStore("file-uploads");await uploads.set(key, file); // isolated from productionDeploy-scoped stores are cleaned up when the deploy is deleted, so preview data doesn’t pile up.
If you need the data to outlive a single deploy but stay out of production, put the context in the store name:
import { getStore } from "@netlify/blobs";import type { Context } from "@netlify/functions";
export default async (req: Request, context: Context) => { const store = getStore(`uploads-${context.deploy.context}`); // production -> "uploads-production" // deploy preview -> "uploads-deploy-preview" // branch deploy -> "uploads-branch-deploy" ...};context.deploy.context gives you the current deploy context, so each context gets its own store while persisting across deploys within that context. Watch the 64-byte store-name limit if you’re composing longer names.
If you want a hard stop rather than a separate store, gate the destructive path on context:
if (context.deploy.context !== "production") { return new Response("Refusing to run destructive operation outside production", { status: 403 });}Common failure modes
You fixed the code but production data is already gone. Blobs has no built-in versioning or point-in-time restore. Deleted is deleted. Check whether the data exists anywhere else (a database, the original uploads, a downstream system) and rebuild from there. Worth knowing before you need it: downloading a deploy does not include its blobs, so a deploy download isn’t a backup.
You assumed locking the deploy protected the store. It doesn’t. Locking a published deploy prevents new deploys from publishing; it does not prevent writes to that deploy’s deploy-specific stores.
You checked with the CLI and the data looked fine. The CLI always uses strong consistency, while your function reads eventually-consistent by default. Deletes and updates propagate within 60 seconds, so the CLI and your function can disagree for up to a minute. Neither one is lying to you.
You switched to getDeployStore() and your existing data disappeared. Expected. It’s a different store. Data written through getStore() is still in the site-scoped store; you’ll need to copy across what you want to keep.
You tried to enumerate deploy stores to audit the damage. listStores() excludes deploy-specific stores, so it won’t show them.
You audited your own code and found nothing. Check your build plugins too. Plugins can read from any of the site’s stores. They can only write to deploy-specific stores, so they can’t cause the destructive version of this, but a plugin reading production data into a preview build is still worth knowing about.
Reference
Store scope
| Call | Scope | Persists across deploys | Cleaned up on deploy deletion |
|---|---|---|---|
getStore(name) | Entire site, all contexts | Yes | No |
getDeployStore(name) | Single deploy | No | Yes |
getStore(`${name}-${context.deploy.context}`) | One deploy context | Yes | No |
Deploy context values
context.deploy.context returns the current context: production, deploy-preview, branch-deploy, preview-server, or dev.
What doesn’t isolate a store
| Not a safeguard | Why |
|---|---|
onlyIfNew / onlyIfMatch | Concurrency guards, not context guards |
| Locking a published deploy | Doesn’t prevent writes to deploy-specific stores |
| Netlify Dev’s sandbox | Only applies locally; a deployed preview reaches the real store |
listStores() | Excludes deploy-specific stores, so it can’t audit them |
| Downloading a deploy | Does not include blobs |
Related limits
- Store names: no
/, no:, max 64 bytes - Default consistency: eventual. Updates and deletes propagate within 60 seconds.
deleteAll()returns{ deletedBlobs }, or0if the store didn’t exist
FAQs
Can a Netlify deploy preview write to production Blobs data?
Yes, if the code calls getStore(name). Site-scoped stores are shared across every deploy context, so preview code reads, overwrites, and deletes production data. Use getDeployStore(name) to isolate a deploy.
What’s the difference between getStore and getDeployStore?
getStore opens one store shared by the whole site across all contexts. getDeployStore opens a store scoped to the running deploy, isolated from production and cleaned up when the deploy is deleted.
How do I give each deploy context its own Blobs store?
Either use getDeployStore(), or include the context in the store name so each context resolves to its own store (see the example above). Keep the name under 64 bytes.
Can I recover Blobs data deleted from a deploy preview? No. Blobs has no versioning or point-in-time restore, and a deploy download doesn’t include blobs. Rebuild from another source if you have one.
Why didn’t this show up in local testing? Netlify Dev uses a sandboxed local store that can’t read or write production data. The shared-store behavior only appears once the code runs on a real deploy.
Does locking a deploy protect its Blobs data? No. Locking stops new deploys from publishing. It doesn’t prevent writes to deploy-specific stores.
Is onlyIfMatch enough to make preview writes safe?
No. It’s a concurrency guard against competing writers, not a check on which deploy context you’re in. Scope the store instead.
Related
This article is generated from Netlify’s open-source agent guidance at netlify/context-and-tools, the same reference our AI coding agents use. The store-scoping warning above is flagged “READ THIS FIRST” in the source.
Building something that needs storage? Start at netlify.new.