{"id":13341,"plugin_id":"plugin_asdk_app_6a79c45722208191816dac268a720cb5","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:07:19.808Z","digest":"355fcf356f2a4b66a9974056f0b3fdaf3991bd6203d4c773a7dce6d2d0685fbe","against":null,"payload":{"name":"upstash-blob-js","description":"Work with the @upstash/blob TypeScript/JavaScript SDK for S3-compatible object storage with direct browser uploads, presigned URLs, multipart, and signed reads. Use when storing files or blobs, uploading avatars, images, videos, attachments or user documents, letting a browser upload straight to storage without proxying bytes through a server, generating public or time-limited signed URLs, serving private files, streaming large files with pause and resume, setting cache headers on stored objects, or reaching an S3-compatible bucket from the AWS SDK.","included_files":[],"skill_md_contents":"---\nname: upstash-blob-js\ndescription: Work with the @upstash/blob TypeScript/JavaScript SDK for S3-compatible object storage with direct browser uploads, presigned URLs, multipart, and signed reads. Use when storing files or blobs, uploading avatars, images, videos, attachments or user documents, letting a browser upload straight to storage without proxying bytes through a server, generating public or time-limited signed URLs, serving private files, streaming large files with pause and resume, setting cache headers on stored objects, or reaching an S3-compatible bucket from the AWS SDK.\nlicense: MIT\nmetadata:\n  author: Upstash\n  homepage: https://upstash.com\n---\n\n# @upstash/blob SDK\n\nS3-compatible object storage. `Bucket` runs on your server; `uploadHandler` plus React hooks upload from the browser straight to storage so the bytes never pass through your app.\n\n## Install & Setup\n\n```bash\nnpm install @upstash/blob\n```\n\nCreate a bucket in the console and set `UPSTASH_BLOB_TOKEN`. The token is a bearer secret for the whole bucket — keep it server side, never in `NEXT_PUBLIC_` or any bundler-inlined variable.\n\nA bucket is **public** (every object has a URL) or **private** (no URL; reads go through `signedReadUrl`). That is a console setting, not a client option — the SDK learns it from the backend.\n\n```ts\nimport { Bucket } from \"@upstash/blob\"\n\nexport const bucket = Bucket.fromEnv()                          // reads UPSTASH_BLOB_TOKEN\nBucket.fromEnv({ cache: \"immutable\" })                        // default variable, plus options\nBucket.fromEnv(\"MEDIA_TOKEN\", { cache: \"immutable\" })           // another variable, plus options\nnew Bucket({ token: env.UPSTASH_BLOB_TOKEN })                   // Workers: no process.env\n```\n\n## Writing\n\n```ts\nconst blob = await bucket.put(\"reports/q3.pdf\", pdf, { contentType: \"application/pdf\" })\nblob.url            // public URL, undefined on a private bucket\nblob.versionedUrl   // url + ?v=<etag>, changes whenever the bytes do\nblob.etag           // what ifUnchanged takes\n```\n\nBodies: `Request`, `Blob`/`File`, `ArrayBuffer`, typed array, `string`, `ReadableStream`. A stream carries no length — pass `size` (exact, streams through) or `maxSize` (buffers up to the cap), or `put` throws `length_required`.\n\n| Option | Default | What it does |\n|--------|---------|--------------|\n| `contentType` | the body's, else `application/octet-stream` | What the object is stored as |\n| `contentTypes` | any | Allow list, e.g. `[\"image/*\", \"application/pdf\"]` |\n| `maxSize` | none | Refuse a bigger body with `too_large` |\n| `cache` | bucket default | `Cache-Control` stored with the object |\n| `metadata` | none | `x-amz-meta-*`; lowercase keys, printable ASCII values |\n| `allowOverwrite` | `true` | `false` refuses if something is there (`already_exists`) |\n| `ifUnchanged` | none | An etag; fails with `conflict` if it changed |\n| `multipart` | `'16mb'` | Threshold for going up in parts; `true`/`false` force it |\n\nSizes are **decimal**: `'20mb'` is 20,000,000 bytes. `'5mib'` throws.\n\n```ts\nimport { uniquePath } from \"@upstash/blob\"\n\nuniquePath`${user.id}/${file.name}`   // 'u7/holiday-pic-3xK9mBqR.png'\n```\n\nUse `uniquePath` for any value you don't control. Each `${}` becomes one slugged filename that can never add a directory, and the finished path gets a random suffix — so two uploads of `photo.png` never collide. The literal parts of the template are passed through as written, so keep `.` and `..` out of them yourself: a path with those segments is refused later, by the call that uses it, with a `TypeError` rather than a `BlobError`.\n\n`bucket.copy(from, to, { contentType, cache, metadata })` and `bucket.move(from, to, options)` preserve source properties you omit. `bucket.updateJson(path, fn, { maxAttempts: 6 })` retries a read-modify-write on conflict with backoff.\n\n## Reading\n\n```ts\nconst res = await bucket.get(\"reports/q3.pdf\")   // record + body: ReadableStream\nconst info = await bucket.info(\"reports/q3.pdf\") // same record, no bytes (HEAD)\nawait bucket.exists(\"avatars/u7.png\")            // boolean instead of a throw\nconst page = await bucket.list({ prefix: \"avatars/\", limit: 1000 })\n```\n\n`get`/`info` throw `not_found`. Nothing is buffered — wrap the stream to read it:\n\n```ts\nawait new Response((await bucket.get(\"notes/1.md\")).body).text()\n```\n\n`list` pages with `page.cursor` (set only while more remains) and carries no `contentType` or `metadata`. `prefix` is the only filter — **keep your own table as the index** and treat the bucket as storage, not a queryable store.\n\n```ts\nconst { url, expiresAt } = await bucket.signedReadUrl(\"private/report.pdf\", {\n  expiresIn: \"2m\",\n  downloadAs: \"Report Q3.pdf\",   // save under this name instead of rendering inline\n})\n```\n\nCache the link until `expiresAt`, never a deadline you compute — a link cannot outlive the credential that signed it, so you may get less than you asked for. `await bucket.publicUrl(path)` returns the public URL, `undefined` on a private bucket.\n\n## Deleting\n\n```ts\nawait bucket.del(\"avatars/me.png\")            // one path\nawait bucket.del([\"a.png\", \"b.png\"])          // an array, batched by 1000\nawait bucket.del({ prefix: \"tmp/\" })          // everything under a prefix\n```\n\nAlready-gone counts as success, so deletes are safe to retry. An array or prefix delete where objects survive throws `partial_delete` with them in `e.failed`. `del({ prefix: '' })` is refused unless you pass `all: true`.\n\n## Browser uploads\n\nThe handler authorizes and records; the bytes go browser → storage, so platform request body caps don't apply. Supply your application's `getUser` and `db.files.upsert` implementations below.\n\n```ts\n// lib/uploads.ts\nimport \"server-only\"\nimport { BlobError, uniquePath, uploadHandler } from \"@upstash/blob\"\nimport { getUser } from \"@/lib/auth\"\nimport { db } from \"@/lib/db\"\n\nexport const uploads = uploadHandler({\n  constraints: { maxSize: \"20mb\", contentTypes: [\"image/*\", \"application/pdf\"] },\n\n  onBeforeUpload: async ({ request, file }) => {\n    const user = await getUser(request)\n    if (!user) throw new BlobError(\"unauthorized\")     // nothing is signed\n    return { path: uniquePath`${user.id}/${file.name}`, metadata: { owner: user.id } }\n  },\n\n  onUploadComplete: async ({ uploadId, path, url, metadata }) => {\n    if (!metadata.owner) throw new BlobError(\"unauthorized\")\n    await db.files.upsert({ id: uploadId, owner: metadata.owner, path, url })\n    return { path }                                    // becomes upload.blob.data\n  },\n})\n```\n\n```ts\n// app/api/upload/route.ts\nimport { uploads } from \"@/lib/uploads\"\n\nexport const { GET, POST } = uploads\n```\n\n```ts\n// lib/upload-hooks.ts\n\"use client\"\nimport { uploadHooks } from \"@upstash/blob/react\"\nimport type { uploads } from \"./uploads\"\n\nexport const { useUpload } = uploadHooks<typeof uploads>()\n```\n\n```tsx\n\"use client\"\nimport { useUpload } from \"@/lib/upload-hooks\"\n\nexport function UploadForm() {\n  const { start, upload, accept } = useUpload()\n\n  return <>\n    <input type=\"file\" accept={accept} onChange={(e) => start({ file: e.target.files?.[0] })} />\n    {upload?.pending && <progress value={upload.percent} max={100} />}\n    {upload?.status === \"done\" && <a href={upload.blob.url}>{upload.blob.data.path}</a>}\n    {upload?.status === \"error\" && <p>{upload.error.message}</p>}\n  </>\n}\n```\n\n`GET` serves the route's constraints, so `accept` fills the file dialog and an oversized file is refused before any request leaves the browser. `uploadHooks<typeof uploads>()` types route names and completion data at compile time; `import type` keeps server code out of the bundle.\n\nThis example assumes a **public** bucket. On a private one `url` is `undefined`, so store `path` instead and hand the client a `bucket.signedReadUrl(path)` when it needs to read.\n\nTwo rules that bite:\n\n- **`onUploadComplete` can run more than once.** The browser retries it, so upsert on `uploadId` rather than inserting.\n- **A throw out of `onUploadComplete` deletes the object.** That is right for a refusal and wrong for a transient database error — catch your own storage errors.\n\nFiles over 16 MB go up in parts, which is what gives `pause()`, `resume()`, per-part retry, and resume-after-reload when the user picks the same file again. `percent` caps at 99 until `status` is `done`; drive UI off `upload.pending`.\n\nUse `multipart: true` on the handler to make every upload multipart. Then a closed tab leaves incomplete parts rather than a stored object nobody recorded, and one cron cleans up:\n\n```ts\nawait bucket.abortStaleMultipartUploads({ olderThan: \"1d\" })\n```\n\nFor routes where the bytes must pass through your app, write an ordinary route calling `bucket.put` and drive it with `useServerUpload` from `@upstash/blob/react`.\n\n## Caching\n\n`cache` is written once, at upload, and stored with the object — changing it means writing the object again.\n\n| Value | Stored |\n|-------|--------|\n| `'immutable'` | `public, max-age=31536000, immutable` |\n| `'revalidate'` | `public, max-age=0, must-revalidate` |\n| `'no-store'` | `no-store` |\n| a duration (`'15m'`, `3600`) | `public, max-age=<seconds>` |\n\n`immutable` needs a path that changes when the bytes do — either `uniquePath` per upload, or a stable path served through `versionedUrl`. On a private bucket `private` replaces `public`.\n\n## Errors\n\n```ts\nimport { BlobError } from \"@upstash/blob\"\n\nif (BlobError.is(e) && e.code === \"not_found\") return null\n```\n\nUse `BlobError.is()`, never `instanceof` — an ESM and a CJS copy are different classes. Codes: `not_found`, `already_exists`, `conflict`, `content_type_not_allowed`, `invalid_input`, `too_large`, `empty_body`, `length_required`, `signature_mismatch`, `unauthorized`, `forbidden`, `rate_limited`, `not_ready`, `partial_delete`, `move_left_a_copy`, `invalid_content_type_pattern`, `mint_backoff`, `request_failed`.\n\nA refusal keeps its code all the way to the browser, so hooks switch on `error.code` rather than status numbers. Bad option values (`'5mib'`, a missing token) throw a `TypeError` where they are written, not a `BlobError` per request.\n\n## S3 clients\n\n```ts\nimport { GetObjectCommand, S3Client } from \"@aws-sdk/client-s3\"\n\nconst config = bucket.s3()\nconst s3 = new S3Client(config)   // endpoint and credentials are async providers\n\n// config.bucket is the underlying bucket id, available nowhere else\nawait s3.send(new GetObjectCommand({ Bucket: config.bucket, Key: \"reports/q3.pdf\" }))\n```\n\nBuckets are S3-compatible. Use this for what the SDK doesn't wrap — byte ranges, conditional GETs, tagging. Pass the providers through as they come so the AWS SDK can refresh an expired credential.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}