vercel-blob
Install this skill
npx skills add jezweb/claude-skillsWorks 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
- Create a Blob store instance in the Vercel dashboard
- Extract the environment token using the Vercel CLI
- Define a Server Action to generate a client upload token
- Implement the client-side component using the blob SDK
- Verify the upload destination and file metadata validation
- 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
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.
π Full skill instructions β original source: jezweb/claude-skills
**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)
- Click "Download" above
- In your project, create the directory:
.agent/skills/vercel-blob/ - Save the file as
SKILL.md - 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