vercel-blob
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.
Best use case
vercel-blob is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using vercel-blob should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/vercel-blob/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How vercel-blob Compares
| Feature / Agent | vercel-blob | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Vercel Blob (Object Storage)
**Status**: Production Ready
**Last Updated**: 2025-10-29
**Dependencies**: None
**Latest Versions**: `@vercel/blob@2.0.0`
---
## Quick Start (3 Minutes)
### 1. Create Blob Store
```bash
# In Vercel dashboard: Storage → Create Database → Blob
vercel env pull .env.local
```
Creates: `BLOB_READ_WRITE_TOKEN`
### 2. Install
```bash
npm install @vercel/blob
```
### 3. 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 `access` level (`public` vs `private`)
- Files are automatically distributed via CDN
---
## The 5-Step Setup Process
### Step 1: Create Blob Store
**Vercel Dashboard**:
1. Project → Storage → Create Database → Blob
2. Copy `BLOB_READ_WRITE_TOKEN`
**Local Development**:
```bash
vercel env pull .env.local
```
Creates `.env.local`:
```bash
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 `BLOB_READ_WRITE_TOKEN` to client
✅ **Set correct access level** - `public` (CDN) or `private` (authenticated access)
✅ **Validate file types and sizes** - Before upload, check MIME type and size
✅ **Use pathname organization** - `avatars/`, `uploads/`, `documents/` for structure
✅ **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 `BLOB_READ_WRITE_TOKEN` to client** - Use `handleUpload()` for client uploads
❌ **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** - `file.jpg` collides, use `${timestamp}-${name}` or UUID
❌ **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**: `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)
**Error**: `Error: Request timeout` 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 `handleUpload()` for large files.
### 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.
---
## 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@^2.0.0` - Vercel Blob SDK
**Optional**:
- `sharp@^0.33.0` - Image processing before upload
- `zod@^3.24.0` - File validation schemas
---
## 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`
**Solution**: Run `vercel env pull .env.local`, ensure `.env.local` in `.gitignore`.
### Problem: File size exceeded (>500MB)
**Solution**: Use multipart upload with `createMultipartUpload()` API.
### Problem: Client upload fails with token error
**Solution**: Ensure using `handleUpload()` server-side, don't expose read/write token to client.
### Problem: Files not deleting
**Solution**: Use exact URL from `put()` response, check `del()` return value.
---
## Complete Setup Checklist
- [ ] Blob store created in Vercel dashboard
- [ ] `BLOB_READ_WRITE_TOKEN` environment variable set
- [ ] `@vercel/blob` package installed
- [ ] File validation implemented (type, size)
- [ ] Client upload uses `handleUpload()` (not direct token)
- [ ] Content-type set for uploads
- [ ] Access level correct (`public` vs `private`)
- [ ] Deletion of old files implemented
- [ ] List pagination handles cursor
- [ ] Tested file upload/download/delete locally and in production
---
**Questions? Issues?**
1. Check official docs: https://vercel.com/docs/storage/vercel-blob
2. Verify environment variables are set
3. Ensure using client upload tokens for client-side uploads
4. Monitor storage usage in Vercel dashboardRelated Skills
vercel-kv
Integrate Redis-compatible Vercel KV for caching, session management, and rate limiting in Next.js applications. Powered by Upstash with strong consistency and TTL support. Use when implementing cache strategies, storing temporary data with expiration, building rate limiters, or troubleshooting missing environment variables, serialization errors, or rate limit issues.
neon-vercel-postgres
Set up serverless Postgres with Neon or Vercel Postgres for Cloudflare Workers/Edge. Includes connection pooling, git-like branching for preview environments, and Drizzle/Prisma integration. Use when: setting up edge Postgres, configuring database branching, or troubleshooting "TCP not supported", connection pool exhausted, SSL config (sslmode=require), or Prisma edge compatibility.
zustand-state-management
Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.
zinc-database
Access ZINC (230M+ purchasable compounds). Search by ZINC ID/SMILES, similarity searches, 3D-ready structures for docking, analog discovery, for virtual screening and drug discovery.
zarr-python
Chunked N-D arrays for cloud storage. Compressed arrays, parallel I/O, S3/GCS integration, NumPy/Dask/Xarray compatible, for large-scale scientific computing pipelines.
youtube-transcript
Download YouTube video transcripts when user provides a YouTube URL or asks to download/get/fetch a transcript from YouTube. Also use when user wants to transcribe or get captions/subtitles from a YouTube video.
xlsx
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
wordpress-plugin-core
Build secure WordPress plugins with core patterns for hooks, database interactions, Settings API, custom post types, REST API, and AJAX. Covers three architecture patterns (Simple, OOP, PSR-4) and the Security Trinity. Use when creating plugins, implementing nonces/sanitization/escaping, working with $wpdb prepared statements, or troubleshooting SQL injection, XSS, CSRF vulnerabilities, or plugin activation errors.
whisper
OpenAI's general-purpose speech recognition model. Supports 99 languages, transcription, translation to English, and language identification. Six model sizes from tiny (39M params) to large (1550M params). Use for speech-to-text, podcast transcription, or multilingual audio processing. Best for robust, multilingual ASR.
weights-and-biases
Track ML experiments with automatic logging, visualize training in real-time, optimize hyperparameters with sweeps, and manage model registry with W&B - collaborative MLOps platform
webapp-testing
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
serving-llms-vllm
Serves LLMs with high throughput using vLLM's PagedAttention and continuous batching. Use when deploying production LLM APIs, optimizing inference latency/throughput, or serving models with limited GPU memory. Supports OpenAI-compatible endpoints, quantization (GPTQ/AWQ/FP8), and tensor parallelism.