netlify-blobs

$npx mdskill add netlify/context-and-tools/netlify-blobs

Modern syntax: import from `@netlify/blobs`, open a store, then call methods on the handle.

SKILL.md

.github/skills/netlify-blobsView on GitHub ↗
---
name: netlify-blobs
description: Store and retrieve unstructured objects, files, and cache-like state on Netlify with the @netlify/blobs module. Use when persisting user file uploads (images/documents), caching computed output from functions or Background Functions, serving downloadable assets, storing JSON blobs keyed by ID, or seeding deploy-specific data. Reach for this for key/value or object storage from Functions, Edge Functions, or Build Plugins — not for per-user, transactional, or relational data (use Netlify DB for that). Triggers include "save an uploaded file", "cache API results", "store generated site map", "key/value store for a function", or "file uploads without a database".
---

# Netlify Blobs

Modern syntax: import from `@netlify/blobs`, open a store, then call methods on the handle.

```ts
import { getStore, getDeployStore, listStores } from "@netlify/blobs";
import type { Context } from "@netlify/functions"; // or "@netlify/edge-functions"
```

In Functions, Edge Functions, and Build Plugins, `siteID`, `deployID`, `token` (and `region` for `getDeployStore`) are injected automatically. Install with `npm install @netlify/blobs`.

**Not a database.** For dynamic, per-user, transactional, or relational data, use Netlify DB. Blobs is for objects, files, and cache-like state. It is optimized for frequent reads and infrequent writes.

**Store scope is a footgun. Read this first.** `getStore` opens a **site-wide store shared across ALL deploy contexts**. Code on a deploy preview reads, overwrites, and deletes production data. Never run destructive tests or seed throwaway data from a preview against a site-wide store. Use `getDeployStore()` or a context-specific store name for isolation.

## Choosing a store type

- `getStore(name)`: site-wide and shared across all deploys. Data persists across deploys, and previews see production data.
- `getDeployStore(name)`: deploy-specific and scoped to one deploy. It stays in sync on rollback and is cleaned up on deploy deletion. Use it for isolation and for anything a failed deploy must not corrupt.
- **Build plugins can READ from any of the site's stores, but WRITE only to deploy-specific stores** (`getDeployStore`). File-based uploads also write only to deploy-specific stores.

## Core writes and reads

```ts
const uploads = getStore("file-uploads");

// set: value is ArrayBuffer | Blob | string
await uploads.set(key, file, { metadata: { country: "Spain" } });

// setJSON: any JSON-serializable value
await uploads.setJSON(key, { hello: "world" });

// get: returns value or null. type: text (default) | json | arrayBuffer | blob | stream
const entry = await uploads.get(key);            // string
const obj = await uploads.get(key, { type: "json" });
if (entry === null) { /* 404 */ }
```

`set`/`setJSON` overwrite an existing key. Both return `{ modified, etag }`. `etag` is omitted when no new entry was generated.

### Persisting a user upload (Function)

```ts
import { getStore } from "@netlify/blobs";
import type { Context } from "@netlify/functions";
import { v4 as uuid } from "uuid";

export default async (req: Request, context: Context) => {
  const form = await req.formData();
  const file = form.get("file") as File;
  const key = uuid();
  const uploads = getStore("file-uploads");
  await uploads.set(key, file, { metadata: { country: context.geo.country.name } });
  return new Response("Submission saved");
};
```

Edge functions are identical except `import type { Context } from "@netlify/edge-functions";`.

### Reading (Function)

```ts
export default async (req: Request, context: Context) => {
  const { key } = context.params;
  const uploads = getStore("file-uploads");
  const entry = await uploads.get(key);
  if (entry === null) return new Response(`Not found: ${key}`, { status: 404 });
  return new Response(entry);
};
```

## Metadata and conditional reads

**Null-check before destructuring.** `getWithMetadata` and `getMetadata` resolve to `null` when the key is absent. Destructuring the return value directly (`const { data } = await store.getWithMetadata(key)`) throws on a missing key. Assign the result to a variable, check for `null`, then read its fields.

```ts
// getWithMetadata: data + metadata + etag
const entry = await uploads.getWithMetadata(key);
if (entry === null) return new Response(`Not found: ${key}`, { status: 404 });
const { data, metadata } = entry;
return new Response(data, { headers: { "X-Country": metadata.country } });

// getMetadata: metadata + etag only, without downloading the blob
const meta = await uploads.getMetadata(key); // { etag, metadata } or null
if (meta === null) { /* missing */ }
```

Both accept `{ consistency, etag, type }`.

**Conditional read:** pass a cached `etag`. If it still matches server-side, `data` is `null`, which means your copy is fresh. Compare the whole ETag value, including surrounding quotes and any weakness prefix.

```ts
const entry = await uploads.getWithMetadata("my-key", { etag: cachedETag });
if (entry !== null && entry.etag === cachedETag) {
  // entry.data is null: cached copy still fresh
}
```

## Concurrency: atomic conditional writes

**Last write wins. There is no concurrency control.** Do NOT build counters, balances, or read-modify-write logic on a blob key, even with `onlyIfMatch` retries. That is transactional data; use Netlify DB.

`set`/`setJSON` accept `{ onlyIfNew, onlyIfMatch }`:

```ts
// Create only if key does not exist
const { modified } = await emails.set("[email protected]", "Jane Doe", { onlyIfNew: true });
if (!modified) return new Response("Email already exists", { status: 400 });

// Update only if the ETag still matches
const { modified } = await emails.set("[email protected]", "New Jane", { onlyIfMatch: etag });
if (!modified) return new Response("Cached data is stale", { status: 400 });
```

## Listing

```ts
const { blobs } = await uploads.list(); // blobs: [{ etag, key }]
```

`list({ directories, paginate, prefix })`. Group keys hierarchically with `/`:

```ts
const { blobs, directories } = await animals.list({ directories: true });
// directories: ["cats", "dogs"]; blobs: top-level keys only

// Drill in. Trailing slash REQUIRED (without it "catsuit" also matches)
const res = await animals.list({ directories: true, prefix: "cats/" });
```

Pagination: `list` returns all pages by default, in pages of up to 1,000 entries. Set `paginate: true` to get an `AsyncIterator`:

```ts
for await (const page of store.list({ paginate: true })) {
  console.log(page.blobs);
}
```

`listStores({ paginate })` returns `{ stores: string[] }` in pages of up to 1,000. It **does not include deploy-specific stores**.

## Deleting

```ts
await uploads.delete(key);                        // resolves undefined
const { deletedBlobs } = await uploads.deleteAll(); // deletes every object = deletes the store
```

## Expiration (no server-side TTL)

Blobs never expire on their own. Store an expiration timestamp in metadata, check it on read, and `delete` the blob once it has passed:

```ts
await uploads.set(key, body, { metadata: { expiration: new Date("2025-01-01").getTime() } });
const entry = await uploads.getWithMetadata(key);
if (entry === null) return new Response("Blob does not exist");
const { expiration } = entry.metadata;
if (expiration && expiration < Date.now()) await uploads.delete(key);
```

## Consistency

The default is **eventual** consistency. New writes are globally available immediately, but updates and deletions take up to 60 seconds to reach all edge locations. Opt into **strong** consistency per store or per read:

```ts
const store = getStore({ name: "animals", consistency: "strong" }); // whole store
await store.get("dog", { consistency: "strong" });                  // single read
```

Netlify CLI always uses strong consistency.

## Regions

`region` takes an **AWS region code**, not the airport code used for the functions region. Supported values are `ap-southeast-1`, `ap-southeast-2`, `eu-central-1`, `us-east-1`, and `us-east-2`. Any other value throws `InvalidBlobsRegionError` before the request is made.

- **Deploy-specific stores** default to your functions region (auto-injected).
- **Site-wide stores** default to `us-east-2` and do NOT follow your functions region.

**Footgun: the site-wide region is per-call.** To keep a site-wide store in a specific region, pass `region` on **every** `getStore` call for that store (reads, writes, deletes). A call that omits it uses `us-east-2` and silently sees no data. There is no error or warning.

**Footgun: changing a region does not move data.** The store appears empty in the new region while its data stays in the old one. To migrate, copy each entry to a store opened in the new region, then delete it from the old one.

```ts
const uploads = getDeployStore({ name: "file-uploads", region: "ap-southeast-2" });
const profiles = getStore({ name: "user-profiles", region: "eu-central-1" });
```

## File-based uploads (no build plugin)

Place files under `.netlify/blobs/deploy` in the base directory. Netlify uploads them to **deploy-specific stores**, preserving directory structure. To attach metadata, add a sibling JSON file named `$<filename>.json`. It must be valid JSON or the deploy fails.

```
.netlify/blobs/deploy/
├─ dogs/good-boy.jpg
├─ dogs/$good-boy.jpg.json   # metadata for good-boy.jpg
├─ cat.jpg
└─ mouse.jpg
```

**Caution:** Netlify empties `.netlify/blobs/deploy` before each build. Files committed to your repo are NOT uploaded. Create blob files during the build, with your build command or a build plugin.

## Access control (default to private)

Blobs have **no built-in access control**: the serving function is the gate. Blobs are only reachable through your own site's code and are encrypted at rest and in transit. When in doubt, default to private and gate reads behind an authenticated function rather than exposing blobs publicly.

For sensitive data, do not serve arbitrary user-supplied keys. Scope keys with something callers cannot tamper with. Blobs is not part of Netlify's HIPAA-compliant offering.

## Constraints

- Store names: no `/` or `:`, max 64 bytes.
- Keys: non-empty, cannot start with `/`, max 600 bytes. Any Unicode is allowed, but some characters take more than 1 byte.
- Object size max 5 GB; metadata max 2 KB.
- Functions written in **Go cannot access Netlify Blobs**.
- The Fetch API is required (Node.js 18+). Otherwise pass a custom `fetch`: `getStore({ fetch, name: "file-uploads" })`.
- Local dev (Netlify Dev) uses a sandboxed local store. It does not support file-based uploads and cannot read production data.
- File-based uploads require continuous deployment or CLI deploys.

## When an operation fails

Surface the error and read the function logs. Do not invent REST endpoints or side-channel APIs to retry.

## CLI and UI

`netlify blobs:list/get/set/delete` exist for inspection. See the [CLI command reference](https://cli.netlify.com/commands/blobs/). To browse and download blobs in the UI, go to **Data & Storage > Blobs**.

## Module version migration

Site-wide stores written with `@netlify/blobs` 6.5.0 or earlier become inaccessible after upgrading, because of a namespacing change. Migrate them with the latest CLI, then use module 7.0.0+:

```sh
netlify recipes blobs-migrate YOUR_STORE_NAME
```

## Reference

Full API and background: [Netlify Blobs docs](https://docs.netlify.com/build/data-and-storage/netlify-blobs/) and the [data & storage overview](https://docs.netlify.com/build/data-and-storage/overview/).

<!-- system: agent-context/blobs/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->
# Netlify house rules (blobs)

These are org conventions, not docs facts — merged into the rendered skill by
ctx-gen and never generated. Owned by the skills maintainer.

1. Blobs is not a database. For dynamic, per-user, or transactional data,
   use Netlify DB — Blobs is for objects, files, and cache-like state.
2. When a store operation fails, surface the error and read the function
   logs — do not invent REST endpoints or side-channel APIs to retry.
3. `netlify blobs:list/get/set/delete` exist for inspection; the CLI
   reference is their source of truth — link, don't restate.
4. Blobs have no built-in access control — the serving function is the gate.
   When in doubt, default to private: gate reads behind an authenticated
   function rather than exposing blobs publicly.
5. Site-scoped stores are shared across ALL deploy contexts — code on a
   deploy preview reads, overwrites, and deletes production data. Never run
   destructive tests or seed throwaway data from previews; use
   `getDeployStore()` or a context-specific store name for isolation.
6. Don't build counters, balances, or read-modify-write logic on a blob key —
   even with `onlyIfMatch` retries. That's transactional data; use Netlify DB.
7. Build plugins: state BOTH halves — they can read from any of the site's
   stores, but write only to deploy-specific stores (`getDeployStore`).

More from netlify/context-and-tools

SkillDescription
netlify-access-controlPicks the right Netlify site-protection layer and disambiguates the three unrelated "auth" concepts users conflate — app-user login (Netlify Identity), site-load gating (Password Protection / project visibility), and dashboard SAML SSO. Use it when asked to password-protect a site or Deploy Preview, make a project private/public, restrict a site to your team, require SSO to view a site, set up company-wide or internal-app SSO, invite users to a private project, or know which team member made a request to a private site. Also use it for SSO-session symptoms on protected sites: "logged out mid-session", 401s after about an hour, or token expiry/refresh questions. Not for wiring auth code — route app-login setup to netlify-identity.
netlify-agent-runnerRun AI agent tasks remotely on Netlify using Claude, Codex, or Gemini. Use when the user wants to run an AI agent on their site, get a second opinion from another model, or delegate development tasks to run remotely against their repo.
netlify-ai-gatewayUse Netlify AI Gateway to call OpenAI, Anthropic Claude, Google Gemini, TypeSafe (Jev), or OpenRouter-hosted models (xAI/DeepSeek/Meta/Mistral/Qwen) from Netlify Functions or Edge Functions without managing provider accounts or API keys. Reach for this when adding an AI feature to a Netlify app — a chatbot, text summarizer, image generator, joke/content generator, form-submission routing or analysis, or any LLM call — or when wiring the OpenAI/Anthropic/Gemini/TypeSafe/OpenRouter SDK into a Netlify Function, choosing which env vars to use, streaming long generations, or debugging why gateway calls fail at build time or return 401.
netlify-cachingCache dynamic and static responses on Netlify's CDN from Functions, Edge Functions, and proxies. Use when you add caching or cache-control headers to a function response, tune cache TTL or stale-while-revalidate, set up the durable cache, vary a cache key by query/header/cookie/country/language, purge or invalidate the cache by site or cache tag, use the programmatic Cache API (caches.open/match/put) or @netlify/cache helpers (fetchWithCache/cacheHeaders/getCacheStatus), speed up an expensive API call, add ISR or on-demand revalidation, or debug why a response is or isn't cached via the Cache-Status header.
netlify-configConfigure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response headers or basic auth, managing environment variables and secrets, scoping vars per deploy context, marking a var as secret, disabling secret scanning, configuring functions bundling, ignoring builds, or wiring up a monorepo or JavaScript SPA on Netlify.
netlify-databaseZero-config Postgres for Netlify apps via @netlify/database — querying data from Functions/Edge Functions, writing schema migrations, setting up Drizzle ORM, local dev with netlify dev, database branches for deploy previews, and migrating an existing Postgres project onto Netlify. Use when adding a database, building a contact form or CRUD API, writing SQL migrations, wiring up Drizzle, running netlify database commands, testing with a local Postgres, or switching from Neon/Supabase/RDS to Netlify Database.
netlify-deployCreate, configure, and manage Netlify deploys from code — reach for this when setting up Git continuous deployment, running netlify deploy or netlify deploy --prod from the CLI, writing netlify.toml deploy contexts, adding a Deploy to Netlify button, wiring build hooks, configuring Deploy Previews or branch deploys, locking or skipping deploys, fixing a failed or secrets-scanning deploy, or when someone asks to "deploy my site", "set up preview deploys", "add per-branch build config", "keep staging out of Google", or "add a deploy button to my README".
netlify-edge-functionsWrite, route, cache, and debug Netlify Edge Functions, the Deno-based TypeScript or JavaScript handlers in netlify/edge-functions that run at the network edge in front of static files and serverless functions. Activate this skill whenever someone wants to add auth middleware or a login gate, redirect or rewrite visitors by country or cookie, localize or personalize a page with geolocation, run A/B testing, transform or inject HTML into a response, stream server-side rendering at the edge, or cache an edge response. Also use it when an edge function silently does nothing, runs in the wrong order, ignores its Cache-Control headers, or cannot read an environment variable.
netlify-formsServerless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; setting up submission notifications or ChatGPT automations on form submission events; or debugging missing submissions and forms that silently fail to register.
netlify-frameworksDeploy and configure web frameworks on Netlify — build settings and SSR/edge adapters plus local platform emulation and env vars. Use when setting up or fixing a framework deploy (Next.js / Astro / Nuxt / SvelteKit / Remix / React Router / TanStack Start / SolidStart / Gatsby / Angular / Vite / Express / Hydrogen / Hugo / Eleventy / Vue / React), adding SSR or edge functions or middleware wired to Netlify context, fixing SPA redirect and catch-all rules, setting a build command or publish directory, or debugging "why isn't my env var updating" and framework build failures.