Back to Backend Development

vercel-blob

vercelblobcloud storagefile uploadnextjsserver actionsclient uploadstorage
⭐ 860πŸ“„ MITπŸ•’ 2026-06-11Source β†—

Install this skill

npx skills add jezweb/claude-skills

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

Vercel Blob provides an interface for managing file storage directly within your Next.js and Vercel projects. It abstracts the complexities of direct S3 interaction by offering a straightforward SDK for uploading, listing, and removing assets. The service handles public and private access control, CDN distribution, and multipart uploads for large files. By integrating with Vercel's serverless infrastructure, it allows developers to manage binary data without maintaining separate storage servers or complex authentication middleware. The implementation distinguishes between secure client-side uploads and server-side operations, ensuring that sensitive environment tokens remain shielded from the browser. It is primarily built to manage media assets, user-generated content, and documents within a web application ecosystem, focusing on predictable performance and simple path-based file organization.

When to Use This Skill

  • β€’Storing user-uploaded profile pictures and media avatars
  • β€’Hosting application assets like PDFs, documents, or static media
  • β€’Implementing file-sharing features for enterprise internal tools
  • β€’Managing large video uploads through chunked streams

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • β€œhow do I upload files to Vercel storage
  • β€œsecurely upload images from a Next.js form
  • β€œhandle large file uploads in Vercel Blob
  • β€œlist all files in my Vercel blob storage directory
  • β€œdelete a file from Vercel Blob via code

Pro Tips

  • πŸ’‘Always use the `handleUpload()` utility for client-side uploads to prevent exposing the sensitive `BLOB_READ_WRITE_TOKEN` to the browser environment.
  • πŸ’‘Define `allowedContentTypes` and `maximumSizeInBytes` within `onBeforeGenerateToken` to enforce strict validation and resource limits on client-initiated uploads.
  • πŸ’‘Leverage Vercel Server Actions to encapsulate all Blob storage operations, enhancing security by keeping API keys server-side and simplifying data handling logic.

What this skill does

  • β€’Secure presigned token generation for client-side uploads
  • β€’Cursor-based pagination for listing large file directories
  • β€’Multipart upload support for files exceeding 500MB
  • β€’Configurable access control between public and private visibility
  • β€’Automatic content-type detection and storage management

When not to use it

  • βœ•Storing highly sensitive or database-dependent application state
  • βœ•Serving massive global datasets that require specialized CDN edge logic
  • βœ•Applications requiring high-frequency disk I/O operations

Example workflow

  1. Create a Blob store instance in the Vercel dashboard
  2. Extract the environment token using the Vercel CLI
  3. Define a Server Action to generate a client upload token
  4. Implement the client-side component using the blob SDK
  5. Verify the upload destination and file metadata validation
  6. Execute the file upload and store the returned URL in your database

Prerequisites

  • –Active Vercel account
  • –Vercel CLI installed for environment management
  • –Next.js project or Node.js environment

Pitfalls & limitations

  • !Exposing the BLOB_READ_WRITE_TOKEN in browser code creates a critical security risk
  • !Ignoring pagination cursors when listing files leads to incomplete data retrieval
  • !Failing to implement multipart uploads for files over 500MB causes request timeouts
  • !Missing explicit MIME type definitions can cause incorrect browser handling of files

FAQ

Is it safe to upload files directly from the browser?
Yes, provided you use the handleUpload utility to generate a scoped, presigned token. Never expose the main READ_WRITE_TOKEN on the client side.
How do I handle files larger than 500MB?
You must implement the multipart upload API to chunk the file into smaller pieces and finalize the upload upon completion.
Why is my file downloading instead of opening in the browser?
This usually happens when the content-type is not correctly inferred. Ensure you explicitly set the contentType option when using the put method.

How it compares

Unlike manual AWS S3 implementations that require complex SDK configuration and IAM role management, Vercel Blob integrates natively with environment variables and existing serverless runtime constraints.

Source & trust

⭐ 860 starsπŸ“„ MITπŸ•’ Updated 2026-06-11
πŸ“„ Full skill instructions β€” original source: jezweb/claude-skills
# Vercel Blob

**Last Updated**: 2026-01-21
**Version**: @vercel/[email protected]
**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](https://vercel.com/docs/limits) + [4.5MB serverless limit](https://vercel.com/docs/limits) + [Community Discussion](https://github.com/payloadcms/payload/discussions/7569)
**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](https://github.com/vercel/storage/issues/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/[email protected]](https://github.com/vercel/storage/releases/tag/%40vercel/blob%402.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](https://github.com/vercel/storage/issues/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](https://github.com/vercel/storage/issues/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](https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#manual) + [Community Discussion](https://community.vercel.com/t/4-5-mb-payload-limit/10500)
**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](https://github.com/vercel/storage/issues/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' });

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/vercel-blob/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/jezweb/claude-skills/vercel-blob/SKILL.md
  • Cursor: ~/.cursor/skills/jezweb/claude-skills/vercel-blob/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/jezweb/claude-skills/vercel-blob/SKILL.md

πŸš€ Install with CLI:
npx skills add jezweb/claude-skills

Read the Master Guide: Mastering Agent Skills β†’

Related Skill Units

Recommended Rules

View more rules β†’

Recommended Workflows

View more workflows β†’

Recommended MCP Servers

View more MCP servers β†’

Take It Further

Maximize your productivity with these powerful resources

πŸ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
πŸ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid backend development issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Backend Development and is published by JezWeb, maintained in jezweb/claude-skills.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.