Claude
Skills
Sign in
Back

vercel-blob

Included with Lifetime
$97 forever

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.

Image & Videoscriptsassets

What this skill does


# Vercel Blob (Object Storage)

**Status**: Production Ready
**Last Updated**: 2025-10-29
**Dependencies**: None
**Latest Versions**: `@vercel/[email protected]`

---

## 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` dire
Files: 10
Size: 51.3 KB
Complexity: 83/100
Category: Image & Video

Related in Image & Video