vercel-blob
Original:🇺🇸 English
Translated
1 scriptsChecked / no sensitive code detected
Integrate Vercel Blob object storage for file uploads, image management, and CDN-delivered assets in Next.js applications. Supports client-side uploads with presigned URLs and multipart transfers. Use when implementing file uploads (images, PDFs, videos), managing user-generated content, or troubleshooting missing tokens, size limit errors, or client upload failures.
1installs
Sourceovachiever/droid-tings
Added on
NPX Install
npx skill4agent add ovachiever/droid-tings vercel-blobTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Vercel Blob (Object Storage)
Status: Production Ready
Last Updated: 2025-10-29
Dependencies: None
Latest Versions:
@vercel/blob@2.0.0Quick Start (3 Minutes)
1. Create Blob Store
bash
# In Vercel dashboard: Storage → Create Database → Blob
vercel env pull .env.localCreates:
BLOB_READ_WRITE_TOKEN2. Install
bash
npm install @vercel/blob3. Upload File (Server Action)
typescript
'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' // or 'private'
});
return blob.url; // https://xyz.public.blob.vercel-storage.com/file.jpg
}CRITICAL:
- Use client upload tokens for direct client uploads (don't expose )
BLOB_READ_WRITE_TOKEN - Set correct level (
accessvspublic)private - Files are automatically distributed via CDN
The 5-Step Setup Process
Step 1: Create Blob Store
Vercel Dashboard:
- Project → Storage → Create Database → Blob
- Copy
BLOB_READ_WRITE_TOKEN
Local Development:
bash
vercel env pull .env.localCreates :
.env.localbash
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxx"Key Points:
- Free tier: 100GB bandwidth/month
- File size limit: 500MB per file
- Automatic CDN distribution
- Public files are cached globally
Step 2: Server-Side Upload
Next.js Server Action:
typescript
'use server';
import { put } from '@vercel/blob';
export async function uploadAvatar(formData: FormData) {
const file = formData.get('avatar') as File;
// Validate file
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
if (file.size > 5 * 1024 * 1024) {
throw new Error('Max file size: 5MB');
}
// Upload
const blob = await put(`avatars/${Date.now()}-${file.name}`, file, {
access: 'public',
addRandomSuffix: false
});
return {
url: blob.url,
pathname: blob.pathname
};
}API Route (Edge Runtime):
typescript
import { put } from '@vercel/blob';
export const runtime = 'edge';
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File;
const blob = await put(file.name, file, { access: 'public' });
return Response.json(blob);
}Step 3: Client-Side Upload (Presigned URLs)
Create Upload Token (Server Action):
typescript
'use server';
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
export async function getUploadToken(filename: string) {
const jsonResponse = await handleUpload({
body: {
type: 'blob.generate-client-token',
payload: {
pathname: `uploads/${filename}`,
access: 'public',
onUploadCompleted: {
callbackUrl: `${process.env.NEXT_PUBLIC_URL}/api/upload-complete`
}
}
},
request: new Request('https://dummy'),
onBeforeGenerateToken: async (pathname) => {
// Optional: validate user permissions
return {
allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp'],
maximumSizeInBytes: 5 * 1024 * 1024 // 5MB
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
console.log('Upload completed:', blob.url);
}
});
return jsonResponse;
}Client Upload:
typescript
'use client';
import { upload } from '@vercel/blob/client';
import { getUploadToken } from './actions';
export function UploadForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const file = (form.elements.namedItem('file') as HTMLInputElement).files?.[0];
if (!file) return;
const tokenResponse = await getUploadToken(file.name);
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: tokenResponse.url
});
console.log('Uploaded:', blob.url);
}
return (
<form onSubmit={handleSubmit}>
<input type="file" name="file" required />
<button type="submit">Upload</button>
</form>
);
}Step 4: List, Download, Delete
List Files:
typescript
import { list } from '@vercel/blob';
const { blobs } = await list({
prefix: 'avatars/',
limit: 100
});
// Returns: { url, pathname, size, uploadedAt, ... }[]List with Pagination:
typescript
let cursor: string | undefined;
const allBlobs = [];
do {
const { blobs, cursor: nextCursor } = await list({
prefix: 'uploads/',
cursor
});
allBlobs.push(...blobs);
cursor = nextCursor;
} while (cursor);Download File:
typescript
// Files are publicly accessible if access: 'public'
const url = 'https://xyz.public.blob.vercel-storage.com/file.pdf';
const response = await fetch(url);
const blob = await response.blob();Delete File:
typescript
import { del } from '@vercel/blob';
await del('https://xyz.public.blob.vercel-storage.com/file.jpg');
// Or delete multiple
await del([url1, url2, url3]);Step 5: Streaming & Multipart
Streaming Upload:
typescript
import { put } from '@vercel/blob';
import { createReadStream } from 'fs';
const stream = createReadStream('./large-file.mp4');
const blob = await put('videos/large-file.mp4', stream, {
access: 'public',
contentType: 'video/mp4'
});Multipart Upload (Large Files >500MB):
typescript
import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';
// 1. Start multipart upload
const upload = await createMultipartUpload('large-video.mp4', {
access: 'public'
});
// 2. Upload parts (chunks)
const partSize = 100 * 1024 * 1024; // 100MB chunks
const parts = [];
for (let i = 0; i < totalParts; i++) {
const chunk = getChunk(i, partSize);
const part = await uploadPart(chunk, {
uploadId: upload.uploadId,
partNumber: i + 1
});
parts.push(part);
}
// 3. Complete upload
const blob = await completeMultipartUpload({
uploadId: upload.uploadId,
parts
});Critical Rules
Always Do
✅ Use client upload tokens for client-side uploads - Never expose to client
BLOB_READ_WRITE_TOKEN✅ Set correct access level - (CDN) or (authenticated access)
publicprivate✅ Validate file types and sizes - Before upload, check MIME type and size
✅ Use pathname organization - , , for structure
avatars/uploads/documents/✅ Handle upload errors - Network failures, size limits, token expiration
✅ Clean up old files - Delete unused files to manage storage costs
✅ Set content-type explicitly - For correct browser handling (videos, PDFs)
Never Do
❌ Never expose to client - Use for client uploads
BLOB_READ_WRITE_TOKENhandleUpload()❌ Never skip file validation - Always validate type, size, content before upload
❌ Never upload files >500MB without multipart - Use multipart upload for large files
❌ Never use generic filenames - collides, use or UUID
file.jpg${timestamp}-${name}❌ Never assume uploads succeed - Always handle errors (network, quota, etc.)
❌ Never store sensitive data unencrypted - Encrypt before upload if needed
❌ Never forget to delete temporary files - Old uploads consume quota
Known Issues Prevention
This skill prevents 10 documented issues:
Issue #1: Missing Environment Variable
Error:
Source: https://vercel.com/docs/storage/vercel-blob
Why It Happens: Token not set in environment
Prevention: Run and ensure in .
Error: BLOB_READ_WRITE_TOKEN is not definedvercel env pull .env.local.env.local.gitignoreIssue #2: Client Upload Token Exposed
Error: Security vulnerability, unauthorized uploads
Source: https://vercel.com/docs/storage/vercel-blob/client-upload
Why It Happens: Using directly in client code
Prevention: Use to generate client-specific tokens with constraints.
BLOB_READ_WRITE_TOKENhandleUpload()Issue #3: File Size Limit Exceeded
Error: (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.
Error: File size exceeds limitIssue #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 option, Blob guesses incorrectly
Prevention: Always set or explicit MIME type.
contentTypecontentType: file.typeIssue #5: Public File Not Cached
Error: Slow file delivery, high egress costs
Source: Vercel Blob best practices
Why It Happens: Using for files that should be public
Prevention: Use for publicly accessible files (CDN caching).
access: 'private'access: 'public'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 is undefined.
cursorIssue #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 response, check deletion result.
put()Issue #8: Upload Timeout (Large Files)
Error: for files >100MB
Source: Vercel function timeout limits
Why It Happens: Serverless function timeout (10s free tier, 60s pro)
Prevention: Use client-side upload with for large files.
Error: Request timeouthandleUpload()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: or .
`uploads/${Date.now()}-${file.name}`addRandomSuffix: trueIssue #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 callback
Prevention: Use in to update database/state.
onUploadCompletedonUploadCompletedhandleUpload()Configuration Files Reference
package.json
json
{
"dependencies": {
"@vercel/blob": "^2.0.0"
}
}.env.local
bash
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxxxx"Common Patterns
Pattern 1: Avatar Upload
typescript
'use server';
import { put, del } from '@vercel/blob';
export async function updateAvatar(userId: string, formData: FormData) {
const file = formData.get('avatar') as File;
// Validate
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
// Delete old avatar
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (user?.avatarUrl) {
await del(user.avatarUrl);
}
// Upload new
const blob = await put(`avatars/${userId}.jpg`, file, {
access: 'public',
contentType: file.type
});
// Update database
await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId));
return blob.url;
}Pattern 2: Protected File Upload
typescript
'use server';
import { put } from '@vercel/blob';
import { auth } from '@/lib/auth';
export async function uploadDocument(formData: FormData) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const file = formData.get('document') as File;
// Upload as private
const blob = await put(`documents/${session.user.id}/${file.name}`, file, {
access: 'private' // Requires authentication to access
});
// Store in database with user reference
await db.insert(documents).values({
userId: session.user.id,
url: blob.url,
filename: file.name,
size: file.size
});
return blob;
}Pattern 3: Image Gallery with Pagination
typescript
import { list } from '@vercel/blob';
export async function getGalleryImages(cursor?: string) {
const { blobs, cursor: nextCursor } = await list({
prefix: 'gallery/',
limit: 20,
cursor
});
const images = blobs.map(blob => ({
url: blob.url,
uploadedAt: blob.uploadedAt,
size: blob.size
}));
return { images, nextCursor };
}Dependencies
Required:
- - Vercel Blob SDK
@vercel/blob@^2.0.0
Optional:
- - Image processing before upload
sharp@^0.33.0 - - File validation schemas
zod@^3.24.0
Official Documentation
- Vercel Blob: https://vercel.com/docs/storage/vercel-blob
- Client Upload: https://vercel.com/docs/storage/vercel-blob/client-upload
- SDK Reference: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk
- GitHub: https://github.com/vercel/storage
Package Versions (Verified 2025-10-29)
json
{
"dependencies": {
"@vercel/blob": "^2.0.0"
}
}Production Example
- E-commerce: Product images, user uploads (500K+ files)
- Blog Platform: Featured images, author avatars
- SaaS: Document uploads, PDF generation, CSV exports
- Errors: 0 (all 10 known issues prevented)
Troubleshooting
Problem: BLOB_READ_WRITE_TOKEN is not defined
BLOB_READ_WRITE_TOKEN is not definedSolution: Run , ensure in .
vercel env pull .env.local.env.local.gitignoreProblem: File size exceeded (>500MB)
Solution: Use multipart upload with API.
createMultipartUpload()Problem: Client upload fails with token error
Solution: Ensure using server-side, don't expose read/write token to client.
handleUpload()Problem: Files not deleting
Solution: Use exact URL from response, check return value.
put()del()Complete Setup Checklist
- Blob store created in Vercel dashboard
- environment variable set
BLOB_READ_WRITE_TOKEN - package installed
@vercel/blob - File validation implemented (type, size)
- Client upload uses (not direct token)
handleUpload() - Content-type set for uploads
- Access level correct (vs
public)private - Deletion of old files implemented
- List pagination handles cursor
- Tested file upload/download/delete locally and in production
Questions? Issues?
- Check official docs: https://vercel.com/docs/storage/vercel-blob
- Verify environment variables are set
- Ensure using client upload tokens for client-side uploads
- Monitor storage usage in Vercel dashboard