netlify/context-and-tools/netlify-blobs

Claude Code and agent skill by netlify/context-and-tools

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

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".

Install

npx vskill@latest install netlify/context-and-tools/netlify-blobs

SKILL.md

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("jane@netlify.com", "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("jane@netlify.com", "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. 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 and the data & 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.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
10
Repository forks
4
7-day trend score
12

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 24 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/netlify/context-and-tools/netlify-blobs/badge)](https://verified-skill.com/skills/netlify/context-and-tools/netlify-blobs)

Something wrong with this skill or its page? Tell us.