vercel-blob

Last Updated: 2026-01-21 Version: @vercel/blob@2.0.0 Skill Version: 2.1.0

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vercel-blob" with this command: npx skills add jezweb/claude-skills/jezweb-claude-skills-vercel-blob

Vercel Blob

Last Updated: 2026-01-21 Version: @vercel/blob@2.0.0 Skill Version: 2.1.0

Quick Start

Create Blob store: Vercel Dashboard → Storage → Blob

vercel env pull .env.local # Creates BLOB_READ_WRITE_TOKEN npm install @vercel/blob

Server Upload:

'use server'; import { put } from '@vercel/blob';

export async function uploadFile(formData: FormData) { const file = formData.get('file') as File; const blob = await put(file.name, file, { access: 'public' }); return blob.url; }

CRITICAL: Never expose BLOB_READ_WRITE_TOKEN to client. Use handleUpload() for client uploads.

Client Upload (Secure)

Server Action (generates presigned token):

'use server'; import { handleUpload } from '@vercel/blob/client';

export async function getUploadToken(filename: string) { return await handleUpload({ body: { type: 'blob.generate-client-token', payload: { pathname: uploads/${filename}, access: 'public' } }, request: new Request('https://dummy'), onBeforeGenerateToken: async (pathname) => ({ allowedContentTypes: ['image/jpeg', 'image/png'], maximumSizeInBytes: 5 * 1024 * 1024 }) }); }

Client Component:

'use client'; import { upload } from '@vercel/blob/client';

const tokenResponse = await getUploadToken(file.name); const blob = await upload(file.name, file, { access: 'public', handleUploadUrl: tokenResponse.url });

File Management

List/Delete:

import { list, del } from '@vercel/blob';

// List with pagination const { blobs, cursor } = await list({ prefix: 'uploads/', cursor });

// Delete await del(blobUrl);

Multipart (>500MB):

import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';

const upload = await createMultipartUpload('large-video.mp4', { access: 'public' }); // Upload chunks in loop... await completeMultipartUpload({ uploadId: upload.uploadId, parts });

Critical Rules

Always:

  • ✅ Use handleUpload() for client uploads (never expose BLOB_READ_WRITE_TOKEN )

  • ✅ Validate file type/size before upload

  • ✅ Use pathname organization (avatars/ , uploads/ )

  • ✅ Add timestamp/UUID to filenames (avoid collisions)

Never:

  • ❌ Expose BLOB_READ_WRITE_TOKEN to client

  • ❌ Upload >500MB without multipart

  • ❌ Skip file validation

Known Issues Prevention

This skill prevents 16 documented issues:

Issue #1: Missing Environment Variable

Error: Error: BLOB_READ_WRITE_TOKEN is not defined

Source: https://vercel.com/docs/storage/vercel-blob Why It Happens: Token not set in environment Prevention: Run vercel env pull .env.local and ensure .env.local in .gitignore .

Issue #2: Client Upload Token Exposed

Error: Security vulnerability, unauthorized uploads Source: https://vercel.com/docs/storage/vercel-blob/client-upload Why It Happens: Using BLOB_READ_WRITE_TOKEN directly in client code Prevention: Use handleUpload() to generate client-specific tokens with constraints.

Issue #3: File Size Limit Exceeded

Error: Error: File size exceeds limit (500MB) Source: https://vercel.com/docs/storage/vercel-blob/limits Why It Happens: Uploading file >500MB without multipart upload Prevention: Validate file size before upload, use multipart upload for large files.

Issue #4: Wrong Content-Type

Error: Browser downloads file instead of displaying (e.g., PDF opens as text) Source: Production debugging Why It Happens: Not setting contentType option, Blob guesses incorrectly Prevention: Always set contentType: file.type or explicit MIME type.

Issue #5: Public File Not Cached

Error: Slow file delivery, high egress costs Source: Vercel Blob best practices Why It Happens: Using access: 'private' for files that should be public Prevention: Use access: 'public' for publicly accessible files (CDN caching).

Issue #6: List Pagination Not Handled

Error: Only first 1000 files returned, missing files Source: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list Why It Happens: Not iterating with cursor for large file lists Prevention: Use cursor-based pagination in loop until cursor is undefined.

Issue #7: Delete Fails Silently

Error: Files not deleted, storage quota fills up Source: https://github.com/vercel/storage/issues/150 Why It Happens: Using wrong URL format, blob not found Prevention: Use full blob URL from put() response, check deletion result.

Issue #8: Upload Timeout (Large Files) + Server-Side 4.5MB Limit

Error: Error: Request timeout for files >100MB (server) OR file upload fails at 4.5MB (serverless function limit) Source: Vercel function timeout limits + 4.5MB serverless limit + Community Discussion Why It Happens:

  • Serverless function timeout (10s free tier, 60s pro) for server-side uploads

  • CRITICAL: Vercel serverless functions have a hard 4.5MB request body limit. Using put() in server actions/API routes fails for files >4.5MB.

Prevention: Use client-side upload with handleUpload() for files >4.5MB OR use multipart upload.

// ❌ Server-side upload fails at 4.5MB export async function POST(request: Request) { const formData = await request.formData(); const file = formData.get('file') as File; // Fails if >4.5MB await put(file.name, file, { access: 'public' }); }

// ✅ Client upload bypasses 4.5MB limit (supports up to 500MB) const blob = await upload(file.name, file, { access: 'public', handleUploadUrl: '/api/upload/token', multipart: true, // For files >500MB, use multipart });

Issue #9: Filename Collisions

Error: Files overwritten, data loss Source: Production debugging Why It Happens: Using same filename for multiple uploads Prevention: Add timestamp/UUID: uploads/${Date.now()}-${file.name} or addRandomSuffix: true .

Issue #10: Missing Upload Callback

Error: Upload completes but app state not updated Source: https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload Why It Happens: Not implementing onUploadCompleted callback Prevention: Use onUploadCompleted in handleUpload() to update database/state.

Issue #11: Client Upload Token Expiration for Large Files

Error: Error: Access denied, please provide a valid token for this resource

Source: GitHub Issue #443 Why It Happens: Default token expires after 30 seconds. Large files (>100MB) take longer to upload, causing token expiration before validation. Prevention: Set validUntil parameter for large file uploads.

// For large files (>100MB), extend token expiration const jsonResponse = await handleUpload({ body, request, onBeforeGenerateToken: async (pathname) => { return { maximumSizeInBytes: 200 * 1024 * 1024, validUntil: Date.now() + 300000, // 5 minutes }; }, });

Issue #12: v2.0.0 Breaking Change - onUploadCompleted Requires callbackUrl (Non-Vercel Hosting)

Error: onUploadCompleted callback doesn't fire when not hosted on Vercel Source: Release Notes @vercel/blob@2.0.0 Why It Happens: v2.0.0 removed automatic callback URL inference from client-side location.href for security. When not using Vercel system environment variables, you must explicitly provide callbackUrl . Prevention: Explicitly provide callbackUrl in onBeforeGenerateToken for non-Vercel hosting.

// v2.0.0+ for non-Vercel hosting await handleUpload({ body, request, onBeforeGenerateToken: async (pathname) => { return { callbackUrl: 'https://example.com', // Required for non-Vercel hosting }; }, onUploadCompleted: async ({ blob, tokenPayload }) => { // Now fires correctly }, });

// For local development with ngrok: // VERCEL_BLOB_CALLBACK_URL=https://abc123.ngrok-free.app

Issue #13: ReadableStream Upload Not Supported in Firefox

Error: Upload never completes in Firefox Source: GitHub Issue #881 Why It Happens: The TypeScript interface accepts ReadableStream as a body type, but Firefox does not support ReadableStream as a fetch body. Prevention: Convert stream to Blob or ArrayBuffer for cross-browser support.

// ❌ Works in Chrome/Edge, hangs in Firefox const stream = new ReadableStream({ /* ... */ }); await put('file.bin', stream, { access: 'public' }); // Never completes in Firefox

// ✅ Convert stream to Blob for cross-browser support const chunks: Uint8Array[] = []; const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const blob = new Blob(chunks); await put('file.bin', blob, { access: 'public' });

Issue #14: Pathname Cannot Be Modified in onBeforeGenerateToken

Error: File uploaded to wrong path despite server-side pathname override attempt Source: GitHub Issue #863 Why It Happens: The pathname parameter in onBeforeGenerateToken cannot be changed. It's set at upload(pathname, ...) time on the client side. Prevention: Construct pathname on client, validate on server. Use clientPayload to pass metadata.

// Client: Construct pathname before upload await upload(uploads/${Date.now()}-${file.name}, file, { access: 'public', handleUploadUrl: '/api/upload', clientPayload: JSON.stringify({ userId: '123' }), });

// Server: Validate pathname matches expected pattern await handleUpload({ body, request, onBeforeGenerateToken: async (pathname, clientPayload) => { const { userId } = JSON.parse(clientPayload || '{}');

// Validate pathname starts with expected prefix
if (!pathname.startsWith(`uploads/`)) {
  throw new Error('Invalid upload path');
}

return {
  allowedContentTypes: ['image/jpeg', 'image/png'],
  tokenPayload: JSON.stringify({ userId }), // Pass to onUploadCompleted
};

}, });

Issue #15: Multipart Upload Minimum Chunk Size (5MB)

Error: Manual multipart upload fails with small chunks Source: Official Docs + Community Discussion Why It Happens: Each part in manual multipart upload must be at least 5MB (except the last part). This conflicts with Vercel's 4.5MB serverless function limit, making manual multipart uploads impossible via server-side routes. Prevention: Use automatic multipart (multipart: true in put() ) or client uploads.

// ❌ Manual multipart upload fails (can't upload 5MB chunks via serverless function) const upload = await createMultipartUpload('large.mp4', { access: 'public' }); // uploadPart() requires 5MB minimum - hits serverless limit

// ✅ Use automatic multipart via client upload await upload('large.mp4', file, { access: 'public', handleUploadUrl: '/api/upload', multipart: true, // Automatically handles 5MB+ chunks });

Issue #16: Missing File Extension Causes Access Denied Error

Error: Error: Access denied, please provide a valid token for this resource

Source: GitHub Issue #664 Why It Happens: Pathname without file extension causes non-descriptive access denied error. Prevention: Always include file extension in pathname.

// ❌ Fails with confusing error await upload('user-12345', file, { access: 'public', handleUploadUrl: '/api/upload', }); // Error: Access denied

// ✅ Extract extension and include in pathname const extension = file.name.split('.').pop(); await upload(user-${userId}.${extension}, file, { access: 'public', handleUploadUrl: '/api/upload', });

Common Patterns

Avatar Upload with Replacement:

'use server'; import { put, del } from '@vercel/blob';

export async function updateAvatar(userId: string, formData: FormData) { const file = formData.get('avatar') as File; if (!file.type.startsWith('image/')) throw new Error('Only images allowed');

const user = await db.query.users.findFirst({ where: eq(users.id, userId) }); if (user?.avatarUrl) await del(user.avatarUrl); // Delete old

const blob = await put(avatars/${userId}.jpg, file, { access: 'public' }); await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId)); return blob.url; }

Protected Upload (access: 'private' ):

const blob = await put(documents/${userId}/${file.name}, file, { access: 'private' });

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwind-v4-shadcn

No summary provided by upstream source.

Repository SourceNeeds Review
2.7K-jezweb
General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
2.5K-jezweb
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review
General

zustand-state-management

No summary provided by upstream source.

Repository SourceNeeds Review
1.2K-jezweb