Update to Netlify
Snapshot Oct 7, 2026 · 00:02 UTC · version 1.6.0
Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.
Instructions updated for netlify-blobs
Instruction wording changed from “Guide for using Netlify Blobs object storage. Use when storing files, images, documents, or simple key-value data without a full database. Covers getStore(), CRUD operations, metadata, listing, deploy-scoped vs site-scoped stores, and lo...” to “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, ser...”. 158 additional added or edited lines are in the evidence.
Observed in instructions or declared skills. Runtime behavior has not been tested.
Product description
Guide for using Netlify Blobs object storage. Use when storing files, images, documents, or simple key-value data without a full database. Covers getStore(), CRUD operations, metadata, listing, deploy-scoped vs site-scoped stores, and lo...
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, ser...
Skill instructions
Guide for using Netlify Blobs object storage. Use when storing files, images, documents, or simple key-value data without a full database. Covers getStore(), CRUD operations, metadata, listing, deploy-scoped vs site-scoped stores, and lo...
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, ser...
Supporting files
[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":312},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686}]
[]
Compare saved observations
Download comparison JSONFull technical diff · 3 changed fields
changed /description
"Guide for using Netlify Blobs object storage. Use when storing files, images, documents, or simple key-value data without a full database. Covers getStore(), CRUD operations, metadata, listing, deploy-scoped vs site-scoped stores, and local development."
"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\"."
changed /included_files
[
{
"relative_path": "LICENSE.txt",
"size_in_bytes": 10776
},
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 312
},
{
"relative_path": "assets/netlify-small.svg",
"size_in_bytes": 1291
},
{
"relative_path": "assets/netlify.png",
"size_in_bytes": 2686
}
][]
changed /skill_md_contents
"---\nname: netlify-blobs\ndescription: Guide for using Netlify Blobs object storage. Use when storing files, images, documents, or simple key-value data without a full database. Covers getStore(), CRUD operations, metadata, listing, deploy-scoped vs site-scoped stores, and local development.\n---\n\n# Netlify Blobs\n\nNetlify Blobs is zero-config object storage available from any Netlify compute (functions, edge functions, framework server routes). No provisioning required.\n\n```bash\nnpm install @netlify/blobs\n```\n\n## Getting a Store\n\n```typescript\nimport { getStore } from \"@netlify/blobs\";\n\nconst store = getStore({ name: \"my-store\" });\n\n// Use \"strong\" consistency when you need immediate reads after writes\nconst store = getStore({ name: \"my-store\", consistency: \"strong\" });\n```\n\n## CRUD Operations\n\nThese are the **only** store methods. Do not invent others.\n\n### Create / Update\n\n```typescript\n// String or binary data\nawait store.set(\"key\", \"value\");\nawait store.set(\"key\", fileBuffer);\n\n// With metadata\nawait store.set(\"key\", data, {\n metadata: { contentType: \"image/png\", uploadedAt: new Date().toISOString() },\n});\n\n// JSON data\nawait store.setJSON(\"key\", { name: \"Example\", count: 42 });\n```\n\n### Read\n\n```typescript\n// Text (default)\nconst text = await store.get(\"key\"); // string | null\n\n// Typed retrieval\nconst json = await store.get(\"key\", { type: \"json\" }); // object | null\nconst stream = await store.get(\"key\", { type: \"stream\" });\nconst blob = await store.get(\"key\", { type: \"blob\" });\nconst buffer = await store.get(\"key\", { type: \"arrayBuffer\" });\n\n// With metadata\nconst result = await store.getWithMetadata(\"key\");\n// { data: any, etag: string, metadata: object } | null\n\n// Metadata only (no data download)\nconst meta = await store.getMetadata(\"key\");\n// { etag: string, metadata: object } | null\n```\n\n### Delete\n\n```typescript\nawait store.delete(\"key\");\n```\n\n### List\n\n```typescript\nconst { blobs } = await store.list();\n// blobs: [{ etag: string, key: string }, ...]\n\n// Filter by prefix\nconst { blobs } = await store.list({ prefix: \"uploads/\" });\n```\n\n## Store Types\n\n- **Site-scoped** (`getStore()`): Persist across all deploys. Use for most cases.\n- **Deploy-scoped** (`getDeployStore()`): Tied to a specific deploy lifecycle.\n\n## Limits\n\n| Limit | Value |\n|---|---|\n| Max object size | 5 GB |\n| Store name max length | 64 bytes |\n| Key max length | 600 bytes |\n\n## Local Development\n\nLocal dev uses a sandboxed store (separate from production). For Vite-based projects, install `@netlify/vite-plugin` to enable local Blobs access. Otherwise, use `netlify dev`.\n\n**Common error**: \"The environment has not been configured to use Netlify Blobs\" — install `@netlify/vite-plugin` or run via `netlify dev`.\n""---\nname: netlify-blobs\ndescription: 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\".\n---\n\n# Netlify Blobs\n\nModern syntax — import from `@netlify/blobs` and open a store, then call methods on the handle:\n\n```ts\nimport { getStore, getDeployStore, listStores } from \"@netlify/blobs\";\nimport type { Context } from \"@netlify/functions\"; // or \"@netlify/edge-functions\"\n```\n\nIn Functions, Edge Functions, and Build Plugins, `siteID`, `deployID`, `token` (and `region` for `getDeployStore`) are injected automatically. Install with `npm install @netlify/blobs`.\n\n**Not a database.** For dynamic, per-user, transactional, or relational data, use Netlify DB. Blobs is for objects, files, and cache-like state, optimized for frequent reads and infrequent writes.\n\n**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.\n\n## Choosing a store type\n\n- `getStore(name)` — site-wide, shared across all deploys. Data persists across deploys; previews see production data.\n- `getDeployStore(name)` — deploy-specific, scoped to one deploy. Kept in sync on rollback, cleaned up on deploy deletion. Use for isolation and for anything a failed deploy must not corrupt.\n- **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.\n\n## Core writes and reads\n\n```ts\nconst uploads = getStore(\"file-uploads\");\n\n// set: value is ArrayBuffer | Blob | string\nawait uploads.set(key, file, { metadata: { country: \"Spain\" } });\n\n// setJSON: any JSON-serializable value\nawait uploads.setJSON(key, { hello: \"world\" });\n\n// get: returns value or null. type: text (default) | json | arrayBuffer | blob | stream\nconst entry = await uploads.get(key); // string\nconst obj = await uploads.get(key, { type: \"json\" });\nif (entry === null) { /* 404 */ }\n```\n\n`set`/`setJSON` overwrite an existing key. Both return `{ modified, etag }` (`etag` omitted when no new entry was generated).\n\n### Persisting a user upload (Function)\n\n```ts\nimport { getStore } from \"@netlify/blobs\";\nimport type { Context } from \"@netlify/functions\";\nimport { v4 as uuid } from \"uuid\";\n\nexport default async (req: Request, context: Context) => {\n const form = await req.formData();\n const file = form.get(\"file\") as File;\n const key = uuid();\n const uploads = getStore(\"file-uploads\");\n await uploads.set(key, file, { metadata: { country: context.geo.country.name } });\n return new Response(\"Submission saved\");\n};\n```\n\nEdge functions are identical except `import type { Context } from \"@netlify/edge-functions\";`.\n\n### Reading (Function)\n\n```ts\nexport default async (req: Request, context: Context) => {\n const { key } = context.params;\n const uploads = getStore(\"file-uploads\");\n const entry = await uploads.get(key);\n if (entry === null) return new Response(`Not found: ${key}`, { status: 404 });\n return new Response(entry);\n};\n```\n\n## Metadata and conditional reads\n\n```ts\n// getWithMetadata: data + metadata + etag; supports conditional reads\nconst { data, etag, metadata } = await uploads.getWithMetadata(key);\n\n// getMetadata: metadata + etag only, without downloading the blob\nconst meta = await uploads.getMetadata(key); // { etag, metadata } or null\n```\n\nBoth return `null` if the key is absent. Both accept `{ consistency, etag, type }`.\n\n**Conditional read:** pass a cached `etag`; if it still matches server-side, `data` is `null` (your copy is fresh). Compare the whole ETag value including surrounding quotes and any weakness prefix.\n\n```ts\nconst { data, etag } = await uploads.getWithMetadata(\"my-key\", { etag: cachedETag });\nif (etag === cachedETag) {\n // data is null — cached copy still fresh\n}\n```\n\n## Concurrency: atomic conditional writes\n\n**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.\n\n`set`/`setJSON` accept `{ onlyIfNew, onlyIfMatch }`:\n\n```ts\n// Create only if key does not exist\nconst { modified } = await emails.set(\"jane@netlify.com\", \"Jane Doe\", { onlyIfNew: true });\nif (!modified) return new Response(\"Email already exists\", { status: 400 });\n\n// Update only if the ETag still matches\nconst { modified } = await emails.set(\"jane@netlify.com\", \"New Jane\", { onlyIfMatch: etag });\nif (!modified) return new Response(\"Cached data is stale\", { status: 400 });\n```\n\n## Listing\n\n```ts\nconst { blobs } = await uploads.list(); // blobs: [{ etag, key }]\n```\n\n`list({ directories, paginate, prefix })`. Group keys hierarchically with `/`:\n\n```ts\nconst { blobs, directories } = await animals.list({ directories: true });\n// directories: [\"cats\", \"dogs\"]; blobs: top-level keys only\n\n// Drill in — trailing slash REQUIRED (without it \"catsuit\" also matches)\nconst res = await animals.list({ directories: true, prefix: \"cats/\" });\n```\n\nPagination: `list` returns all pages by default (pages of up to 1,000 entries). Set `paginate: true` for an `AsyncIterator`:\n\n```ts\nfor await (const page of store.list({ paginate: true })) {\n console.log(page.blobs);\n}\n```\n\n`listStores({ paginate })` returns `{ stores: string[] }` — **does not include deploy-specific stores** (pages of up to 1,000).\n\n## Deleting\n\n```ts\nawait uploads.delete(key); // resolves undefined\nconst { deletedBlobs } = await uploads.deleteAll(); // deletes every object = deletes the store\n```\n\n## Expiration (no server-side TTL)\n\nBlobs never expire on their own. Store an expiration timestamp in metadata, check it on read, and `delete` when past:\n\n```ts\nawait uploads.set(key, body, { metadata: { expiration: new Date(\"2025-01-01\").getTime() } });\nconst entry = await uploads.getWithMetadata(key);\nconst { expiration } = entry.metadata;\nif (expiration && expiration < Date.now()) await uploads.delete(key);\n```\n\n## Consistency\n\nDefault is **eventual** consistency: writes are globally available immediately, but updates/deletions propagate to all edge locations within 60 seconds. Opt into **strong** consistency per store or per read:\n\n```ts\nconst store = getStore({ name: \"animals\", consistency: \"strong\" }); // whole store\nawait store.get(\"dog\", { consistency: \"strong\" }); // single read\n```\n\nNetlify CLI always uses strong consistency.\n\n## Regions\n\n`region` takes an **AWS region code** (not the functions airport code). Supported (any other value throws `InvalidBlobsRegionError` before the request): `ap-southeast-1`, `ap-southeast-2`, `eu-central-1`, `us-east-1`, `us-east-2`.\n\n- **Deploy-specific stores** default to your functions region (auto-injected).\n- **Site-wide stores** default to `us-east-2` and do NOT follow your functions region.\n\n**Footgun — site-wide region is per-call:** if you need 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 — no error or warning.\n\n**Footgun — changing a region does not move data:** the store appears empty in the new region while data remains in the old. To migrate, copy each entry to a store opened in the new region, then delete from the old.\n\n```ts\nconst uploads = getDeployStore({ name: \"file-uploads\", region: \"ap-southeast-2\" });\nconst profiles = getStore({ name: \"user-profiles\", region: \"eu-central-1\" });\n```\n\n## File-based uploads (no build plugin)\n\nPlace files under `.netlify/blobs/deploy` in the base directory; Netlify uploads them (preserving directory structure) to **deploy-specific stores**. Attach metadata with a sibling JSON file named `$<filename>.json` (must be valid JSON or the deploy fails).\n\n```\n.netlify/blobs/deploy/\n├─ dogs/good-boy.jpg\n├─ dogs/$good-boy.jpg.json # metadata for good-boy.jpg\n├─ cat.jpg\n└─ mouse.jpg\n```\n\n**Caution:** Netlify empties `.netlify/blobs/deploy` before each build. Files committed to your repo are NOT uploaded — create blob files during the build (build command or build plugin).\n\n## Access control (default to private)\n\nBlobs have **no built-in access control** — the serving function is the gate. Blobs are only reachable through your own site's code, encrypted at rest and in transit. When in doubt, default to private: gate reads behind an authenticated function rather than exposing blobs publicly. Do not serve arbitrary user-supplied keys for sensitive data; scope keys with something callers cannot tamper with. Blobs is not part of Netlify's HIPAA-compliant offering.\n\n## Constraints\n\n- Store names: no `/` or `:`, max 64 bytes.\n- Keys: non-empty, cannot start with `/`, max 600 bytes, any Unicode (some chars >1 byte).\n- Object size max 5 GB; metadata max 2 KB.\n- Functions written in **Go cannot access Netlify Blobs**.\n- Fetch API required (Node.js 18+); otherwise pass a custom `fetch`: `getStore({ fetch, name: \"file-uploads\" })`.\n- Local dev (Netlify Dev) uses a sandboxed local store: no file-based uploads, cannot read production data.\n- File-based uploads require continuous deployment or CLI deploys.\n\n## When an operation fails\n\nSurface the error and read the function logs. Do not invent REST endpoints or side-channel APIs to retry.\n\n## CLI and UI\n\n`netlify blobs:list/get/set/delete` exist for inspection — see the [CLI command reference](https://cli.netlify.com/commands/blobs/). Browse and download in the UI under **Data & Storage > Blobs**.\n\n## Module version migration\n\nIf you wrote to site-wide stores with `@netlify/blobs` 6.5.0 or earlier and upgrade, those stores become inaccessible due to a namespacing change. Migrate with the latest CLI, then use module 7.0.0+:\n\n```sh\nnetlify recipes blobs-migrate YOUR_STORE_NAME\n```\n\n## Reference\n\nFull 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/).\n\n<!-- system: agent-context/blobs/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (blobs)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Blobs is not a database. For dynamic, per-user, or transactional data,\n use Netlify DB — Blobs is for objects, files, and cache-like state.\n2. When a store operation fails, surface the error and read the function\n logs — do not invent REST endpoints or side-channel APIs to retry.\n3. `netlify blobs:list/get/set/delete` exist for inspection; the CLI\n reference is their source of truth — link, don't restate.\n4. Blobs have no built-in access control — the serving function is the gate.\n When in doubt, default to private: gate reads behind an authenticated\n function rather than exposing blobs publicly.\n5. Site-scoped stores are shared across ALL deploy contexts — code on a\n deploy preview reads, overwrites, and deletes production data. Never run\n destructive tests or seed throwaway data from previews; use\n `getDeployStore()` or a context-specific store name for isolation.\n6. Don't build counters, balances, or read-modify-write logic on a blob key —\n even with `onlyIfMatch` retries. That's transactional data; use Netlify DB.\n7. Build plugins: state BOTH halves — they can read from any of the site's\n stores, but write only to deploy-specific stores (`getDeployStore`).\n"SKILL.md line diff
--- before +++ after @@ -1,99 +1,255 @@ --- name: netlify-blobs -description: Guide for using Netlify Blobs object storage. Use when storing files, images, documents, or simple key-value data without a full database. Covers getStore(), CRUD operations, metadata, listing, deploy-scoped vs site-scoped stores, and local development. +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 -Netlify Blobs is zero-config object storage available from any Netlify compute (functions, edge functions, framework server routes). No provisioning required. +Modern syntax — import from `@netlify/blobs` and open a store, then call methods on the handle: -```bash -npm install @netlify/blobs +```ts +import { getStore, getDeployStore, listStores } from "@netlify/blobs"; +import type { Context } from "@netlify/functions"; // or "@netlify/edge-functions" ``` -## Getting a Store +In Functions, Edge Functions, and Build Plugins, `siteID`, `deployID`, `token` (and `region` for `getDeployStore`) are injected automatically. Install with `npm install @netlify/blobs`. -```typescript +**Not a database.** For dynamic, per-user, transactional, or relational data, use Netlify DB. Blobs is for objects, files, and cache-like state, 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, shared across all deploys. Data persists across deploys; previews see production data. +- `getDeployStore(name)` — deploy-specific, scoped to one deploy. Kept in sync on rollback, cleaned up on deploy deletion. Use 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` 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"); +}; +``` -const store = getStore({ name: "my-store" }); +Edge functions are identical except `import type { Context } from "@netlify/edge-functions";`. -// Use "strong" consistency when you need immediate reads after writes -const store = getStore({ name: "my-store", consistency: "strong" }); +### 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 + +```ts +// getWithMetadata: data + metadata + etag; supports conditional reads +const { data, etag, metadata } = await uploads.getWithMetadata(key); + +// getMetadata: metadata + etag only, without downloading the blob +const meta = await uploads.getMetadata(key); // { etag, metadata } or null +``` + +Both return `null` if the key is absent. Both accept `{ consistency, etag, type }`. + +**Conditional read:** pass a cached `etag`; if it still matches server-side, `data` is `null` (your copy is fresh). Compare the whole ETag value including surrounding quotes and any weakness prefix. + +```ts +const { data, etag } = await uploads.getWithMetadata("my-key", { etag: cachedETag }); +if (etag === cachedETag) { + // data is null — cached copy still fresh +} ``` -## CRUD Operations +## Concurrency: atomic conditional writes -These are the **only** store methods. Do not invent others. +**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. -### Create / Update +`set`/`setJSON` accept `{ onlyIfNew, onlyIfMatch }`: -```typescript -// String or binary data -await store.set("key", "value"); -await store.set("key", fileBuffer); +```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 }); +``` -// With metadata -await store.set("key", data, { - metadata: { contentType: "image/png", uploadedAt: new Date().toISOString() }, -}); +## Listing -// JSON data -await store.setJSON("key", { name: "Example", count: 42 }); +```ts +const { blobs } = await uploads.list(); // blobs: [{ etag, key }] ``` -### Read +`list({ directories, paginate, prefix })`. Group keys hierarchically with `/`: -```typescript -// Text (default) -const text = await store.get("key"); // string | null +```ts +const { blobs, directories } = await animals.list({ directories: true }); +// directories: ["cats", "dogs"]; blobs: top-level keys only -// Typed retrieval -const json = await store.get("key", { type: "json" }); // object | null -const stream = await store.get("key", { type: "stream" }); -const blob = await store.get("key", { type: "blob" }); -const buffer = await store.get("key", { type: "arrayBuffer" }); +// Drill in — trailing slash REQUIRED (without it "catsuit" also matches) +const res = await animals.list({ directories: true, prefix: "cats/" }); +``` -// With metadata -const result = await store.getWithMetadata("key"); -// { data: any, etag: string, metadata: object } | null +Pagination: `list` returns all pages by default (pages of up to 1,000 entries). Set `paginate: true` for an `AsyncIterator`: -// Metadata only (no data download) -const meta = await store.getMetadata("key"); -// { etag: string, metadata: object } | null +```ts +for await (const page of store.list({ paginate: true })) { + console.log(page.blobs); +} ``` -### Delete +`listStores({ paginate })` returns `{ stores: string[] }` — **does not include deploy-specific stores** (pages of up to 1,000). + +## Deleting -```typescript -await store.delete("key"); +```ts +await uploads.delete(key); // resolves undefined +const { deletedBlobs } = await uploads.deleteAll(); // deletes every object = deletes the store ``` -### List +## Expiration (no server-side TTL) -```typescript -const { blobs } = await store.list(); -// blobs: [{ etag: string, key: string }, ...] +Blobs never expire on their own. Store an expiration timestamp in metadata, check it on read, and `delete` when past: -// Filter by prefix -const { blobs } = await store.list({ prefix: "uploads/" }); +```ts +await uploads.set(key, body, { metadata: { expiration: new Date("2025-01-01").getTime() } }); +const entry = await uploads.getWithMetadata(key); +const { expiration } = entry.metadata; +if (expiration && expiration < Date.now()) await uploads.delete(key); ``` -## Store Types +## Consistency + +Default is **eventual** consistency: writes are globally available immediately, but updates/deletions propagate to all edge locations within 60 seconds. Opt into **strong** consistency per store or per read: -- **Site-scoped** (`getStore()`): Persist across all deploys. Use for most cases. -- **Deploy-scoped** (`getDeployStore()`): Tied to a specific deploy lifecycle. +```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 functions airport code). Supported (any other value throws `InvalidBlobsRegionError` before the request): `ap-southeast-1`, `ap-southeast-2`, `eu-central-1`, `us-east-1`, `us-east-2`. + +- **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 — site-wide region is per-call:** if you need 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 — no error or warning. + +**Footgun — changing a region does not move data:** the store appears empty in the new region while data remains in the old. To migrate, copy each entry to a store opened in the new region, then delete from the old. + +```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 (preserving directory structure) to **deploy-specific stores**. Attach metadata with a sibling JSON file named `$<filename>.json` (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 (build command or 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, encrypted at rest and in transit. When in doubt, default to private: gate reads behind an authenticated function rather than exposing blobs publicly. Do not serve arbitrary user-supplied keys for sensitive data; 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 (some chars >1 byte). +- Object size max 5 GB; metadata max 2 KB. +- Functions written in **Go cannot access Netlify Blobs**. +- Fetch API required (Node.js 18+); otherwise pass a custom `fetch`: `getStore({ fetch, name: "file-uploads" })`. +- Local dev (Netlify Dev) uses a sandboxed local store: no file-based uploads, 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/). Browse and download in the UI under **Data & Storage > Blobs**. + +## Module version migration + +If you wrote to site-wide stores with `@netlify/blobs` 6.5.0 or earlier and upgrade, those stores become inaccessible due to a namespacing change. Migrate with the latest CLI, then use module 7.0.0+: + +```sh +netlify recipes blobs-migrate YOUR_STORE_NAME +``` -## Limits +## Reference -| Limit | Value | -|---|---| -| Max object size | 5 GB | -| Store name max length | 64 bytes | -| Key max length | 600 bytes | +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/). -## Local Development +<!-- system: agent-context/blobs/system.md — human-owned, merged by ctx-gen; edit system.md, not this section --> +# Netlify house rules (blobs) -Local dev uses a sandboxed store (separate from production). For Vite-based projects, install `@netlify/vite-plugin` to enable local Blobs access. Otherwise, use `netlify dev`. +These are org conventions, not docs facts — merged into the rendered skill by +ctx-gen and never generated. Owned by the skills maintainer. -**Common error**: "The environment has not been configured to use Netlify Blobs" — install `@netlify/vite-plugin` or run via `netlify dev`. +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`).
Full snapshot data
{
"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\".",
"included_files": [],
"name": "netlify-blobs",
"skill_md_contents": "---\nname: netlify-blobs\ndescription: 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\".\n---\n\n# Netlify Blobs\n\nModern syntax — import from `@netlify/blobs` and open a store, then call methods on the handle:\n\n```ts\nimport { getStore, getDeployStore, listStores } from \"@netlify/blobs\";\nimport type { Context } from \"@netlify/functions\"; // or \"@netlify/edge-functions\"\n```\n\nIn Functions, Edge Functions, and Build Plugins, `siteID`, `deployID`, `token` (and `region` for `getDeployStore`) are injected automatically. Install with `npm install @netlify/blobs`.\n\n**Not a database.** For dynamic, per-user, transactional, or relational data, use Netlify DB. Blobs is for objects, files, and cache-like state, optimized for frequent reads and infrequent writes.\n\n**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.\n\n## Choosing a store type\n\n- `getStore(name)` — site-wide, shared across all deploys. Data persists across deploys; previews see production data.\n- `getDeployStore(name)` — deploy-specific, scoped to one deploy. Kept in sync on rollback, cleaned up on deploy deletion. Use for isolation and for anything a failed deploy must not corrupt.\n- **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.\n\n## Core writes and reads\n\n```ts\nconst uploads = getStore(\"file-uploads\");\n\n// set: value is ArrayBuffer | Blob | string\nawait uploads.set(key, file, { metadata: { country: \"Spain\" } });\n\n// setJSON: any JSON-serializable value\nawait uploads.setJSON(key, { hello: \"world\" });\n\n// get: returns value or null. type: text (default) | json | arrayBuffer | blob | stream\nconst entry = await uploads.get(key); // string\nconst obj = await uploads.get(key, { type: \"json\" });\nif (entry === null) { /* 404 */ }\n```\n\n`set`/`setJSON` overwrite an existing key. Both return `{ modified, etag }` (`etag` omitted when no new entry was generated).\n\n### Persisting a user upload (Function)\n\n```ts\nimport { getStore } from \"@netlify/blobs\";\nimport type { Context } from \"@netlify/functions\";\nimport { v4 as uuid } from \"uuid\";\n\nexport default async (req: Request, context: Context) => {\n const form = await req.formData();\n const file = form.get(\"file\") as File;\n const key = uuid();\n const uploads = getStore(\"file-uploads\");\n await uploads.set(key, file, { metadata: { country: context.geo.country.name } });\n return new Response(\"Submission saved\");\n};\n```\n\nEdge functions are identical except `import type { Context } from \"@netlify/edge-functions\";`.\n\n### Reading (Function)\n\n```ts\nexport default async (req: Request, context: Context) => {\n const { key } = context.params;\n const uploads = getStore(\"file-uploads\");\n const entry = await uploads.get(key);\n if (entry === null) return new Response(`Not found: ${key}`, { status: 404 });\n return new Response(entry);\n};\n```\n\n## Metadata and conditional reads\n\n```ts\n// getWithMetadata: data + metadata + etag; supports conditional reads\nconst { data, etag, metadata } = await uploads.getWithMetadata(key);\n\n// getMetadata: metadata + etag only, without downloading the blob\nconst meta = await uploads.getMetadata(key); // { etag, metadata } or null\n```\n\nBoth return `null` if the key is absent. Both accept `{ consistency, etag, type }`.\n\n**Conditional read:** pass a cached `etag`; if it still matches server-side, `data` is `null` (your copy is fresh). Compare the whole ETag value including surrounding quotes and any weakness prefix.\n\n```ts\nconst { data, etag } = await uploads.getWithMetadata(\"my-key\", { etag: cachedETag });\nif (etag === cachedETag) {\n // data is null — cached copy still fresh\n}\n```\n\n## Concurrency: atomic conditional writes\n\n**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.\n\n`set`/`setJSON` accept `{ onlyIfNew, onlyIfMatch }`:\n\n```ts\n// Create only if key does not exist\nconst { modified } = await emails.set(\"jane@netlify.com\", \"Jane Doe\", { onlyIfNew: true });\nif (!modified) return new Response(\"Email already exists\", { status: 400 });\n\n// Update only if the ETag still matches\nconst { modified } = await emails.set(\"jane@netlify.com\", \"New Jane\", { onlyIfMatch: etag });\nif (!modified) return new Response(\"Cached data is stale\", { status: 400 });\n```\n\n## Listing\n\n```ts\nconst { blobs } = await uploads.list(); // blobs: [{ etag, key }]\n```\n\n`list({ directories, paginate, prefix })`. Group keys hierarchically with `/`:\n\n```ts\nconst { blobs, directories } = await animals.list({ directories: true });\n// directories: [\"cats\", \"dogs\"]; blobs: top-level keys only\n\n// Drill in — trailing slash REQUIRED (without it \"catsuit\" also matches)\nconst res = await animals.list({ directories: true, prefix: \"cats/\" });\n```\n\nPagination: `list` returns all pages by default (pages of up to 1,000 entries). Set `paginate: true` for an `AsyncIterator`:\n\n```ts\nfor await (const page of store.list({ paginate: true })) {\n console.log(page.blobs);\n}\n```\n\n`listStores({ paginate })` returns `{ stores: string[] }` — **does not include deploy-specific stores** (pages of up to 1,000).\n\n## Deleting\n\n```ts\nawait uploads.delete(key); // resolves undefined\nconst { deletedBlobs } = await uploads.deleteAll(); // deletes every object = deletes the store\n```\n\n## Expiration (no server-side TTL)\n\nBlobs never expire on their own. Store an expiration timestamp in metadata, check it on read, and `delete` when past:\n\n```ts\nawait uploads.set(key, body, { metadata: { expiration: new Date(\"2025-01-01\").getTime() } });\nconst entry = await uploads.getWithMetadata(key);\nconst { expiration } = entry.metadata;\nif (expiration && expiration < Date.now()) await uploads.delete(key);\n```\n\n## Consistency\n\nDefault is **eventual** consistency: writes are globally available immediately, but updates/deletions propagate to all edge locations within 60 seconds. Opt into **strong** consistency per store or per read:\n\n```ts\nconst store = getStore({ name: \"animals\", consistency: \"strong\" }); // whole store\nawait store.get(\"dog\", { consistency: \"strong\" }); // single read\n```\n\nNetlify CLI always uses strong consistency.\n\n## Regions\n\n`region` takes an **AWS region code** (not the functions airport code). Supported (any other value throws `InvalidBlobsRegionError` before the request): `ap-southeast-1`, `ap-southeast-2`, `eu-central-1`, `us-east-1`, `us-east-2`.\n\n- **Deploy-specific stores** default to your functions region (auto-injected).\n- **Site-wide stores** default to `us-east-2` and do NOT follow your functions region.\n\n**Footgun — site-wide region is per-call:** if you need 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 — no error or warning.\n\n**Footgun — changing a region does not move data:** the store appears empty in the new region while data remains in the old. To migrate, copy each entry to a store opened in the new region, then delete from the old.\n\n```ts\nconst uploads = getDeployStore({ name: \"file-uploads\", region: \"ap-southeast-2\" });\nconst profiles = getStore({ name: \"user-profiles\", region: \"eu-central-1\" });\n```\n\n## File-based uploads (no build plugin)\n\nPlace files under `.netlify/blobs/deploy` in the base directory; Netlify uploads them (preserving directory structure) to **deploy-specific stores**. Attach metadata with a sibling JSON file named `$<filename>.json` (must be valid JSON or the deploy fails).\n\n```\n.netlify/blobs/deploy/\n├─ dogs/good-boy.jpg\n├─ dogs/$good-boy.jpg.json # metadata for good-boy.jpg\n├─ cat.jpg\n└─ mouse.jpg\n```\n\n**Caution:** Netlify empties `.netlify/blobs/deploy` before each build. Files committed to your repo are NOT uploaded — create blob files during the build (build command or build plugin).\n\n## Access control (default to private)\n\nBlobs have **no built-in access control** — the serving function is the gate. Blobs are only reachable through your own site's code, encrypted at rest and in transit. When in doubt, default to private: gate reads behind an authenticated function rather than exposing blobs publicly. Do not serve arbitrary user-supplied keys for sensitive data; scope keys with something callers cannot tamper with. Blobs is not part of Netlify's HIPAA-compliant offering.\n\n## Constraints\n\n- Store names: no `/` or `:`, max 64 bytes.\n- Keys: non-empty, cannot start with `/`, max 600 bytes, any Unicode (some chars >1 byte).\n- Object size max 5 GB; metadata max 2 KB.\n- Functions written in **Go cannot access Netlify Blobs**.\n- Fetch API required (Node.js 18+); otherwise pass a custom `fetch`: `getStore({ fetch, name: \"file-uploads\" })`.\n- Local dev (Netlify Dev) uses a sandboxed local store: no file-based uploads, cannot read production data.\n- File-based uploads require continuous deployment or CLI deploys.\n\n## When an operation fails\n\nSurface the error and read the function logs. Do not invent REST endpoints or side-channel APIs to retry.\n\n## CLI and UI\n\n`netlify blobs:list/get/set/delete` exist for inspection — see the [CLI command reference](https://cli.netlify.com/commands/blobs/). Browse and download in the UI under **Data & Storage > Blobs**.\n\n## Module version migration\n\nIf you wrote to site-wide stores with `@netlify/blobs` 6.5.0 or earlier and upgrade, those stores become inaccessible due to a namespacing change. Migrate with the latest CLI, then use module 7.0.0+:\n\n```sh\nnetlify recipes blobs-migrate YOUR_STORE_NAME\n```\n\n## Reference\n\nFull 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/).\n\n<!-- system: agent-context/blobs/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (blobs)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Blobs is not a database. For dynamic, per-user, or transactional data,\n use Netlify DB — Blobs is for objects, files, and cache-like state.\n2. When a store operation fails, surface the error and read the function\n logs — do not invent REST endpoints or side-channel APIs to retry.\n3. `netlify blobs:list/get/set/delete` exist for inspection; the CLI\n reference is their source of truth — link, don't restate.\n4. Blobs have no built-in access control — the serving function is the gate.\n When in doubt, default to private: gate reads behind an authenticated\n function rather than exposing blobs publicly.\n5. Site-scoped stores are shared across ALL deploy contexts — code on a\n deploy preview reads, overwrites, and deletes production data. Never run\n destructive tests or seed throwaway data from previews; use\n `getDeployStore()` or a context-specific store name for isolation.\n6. Don't build counters, balances, or read-modify-write logic on a blob key —\n even with `onlyIfMatch` retries. That's transactional data; use Netlify DB.\n7. Build plugins: state BOTH halves — they can read from any of the site's\n stores, but write only to deploy-specific stores (`getDeployStore`).\n"
}SHA-256 of public snapshot: 9ca09ca7af1d5fc9d8df2bc6203ab5cc568ea6a512a7a2a6d68ce9f3392744bc