Skip to main content

Overview

Studley AI uses Vercel Blob for storing user-uploaded files including:
  • Study documents (PDF, DOCX, TXT)
  • Profile pictures
  • Audio recordings
  • Generated content exports
This guide covers setup, implementation, and best practices.

Vercel Blob Setup

1

Create Blob Store

  1. Go to your Vercel Dashboard
  2. Select your Studley AI project
  3. Navigate to Storage tab
  4. Click Create DatabaseBlob
  5. Name: studley-uploads
  6. Click Create
2

Connect to Project

  1. After creation, click Connect to Project
  2. Select your Studley AI project
  3. Choose environment:
    • Production
    • Preview
    • Development (optional)
  4. Click Connect
Vercel automatically adds BLOB_READ_WRITE_TOKEN to your environment variables.
3

Install Dependencies

The @vercel/blob package is already in package.json:
{
  "dependencies": {
    "@vercel/blob": "2.0.0"
  }
}
4

Verify Setup

Check environment variables:
# In Vercel Dashboard
Settings Environment Variables BLOB_READ_WRITE_TOKEN
For local development, add to .env.local:
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxxxxxxxxxxxx"

File Upload Implementation

Basic Upload API Route

Studley AI includes a generic upload endpoint:
app/api/upload/route.ts
import { put } from "@vercel/blob"
import { type NextRequest, NextResponse } from "next/server"

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData()
    const file = formData.get("file") as File
    
    if (!file) {
      return NextResponse.json(
        { error: "No file provided" },
        { status: 400 }
      )
    }
    
    // Convert file to buffer
    const buffer = await file.arrayBuffer()
    
    // Upload to Vercel Blob
    const blob = await put(file.name, buffer, {
      access: "public",  // or "private" for restricted access
    })
    
    return NextResponse.json({
      url: blob.url,
    })
  } catch (error) {
    console.error("Upload error:", error)
    return NextResponse.json(
      { error: "Upload failed" },
      { status: 500 }
    )
  }
}

Avatar Upload

Specialized endpoint for profile pictures:
app/api/upload-avatar/route.ts
import { put } from '@vercel/blob'
import { getSession } from '@/lib/auth/session'

export async function POST(request: NextRequest) {
  const session = await getSession()
  
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  const formData = await request.formData()
  const file = formData.get('file') as File
  
  // Validate file type
  if (!file.type.startsWith('image/')) {
    return NextResponse.json(
      { error: 'Only images allowed' },
      { status: 400 }
    )
  }
  
  // Validate file size (max 5MB)
  if (file.size > 5 * 1024 * 1024) {
    return NextResponse.json(
      { error: 'File too large (max 5MB)' },
      { status: 400 }
    )
  }
  
  const buffer = await file.arrayBuffer()
  
  // Upload with user-specific path
  const blob = await put(`avatars/${session.userId}/${file.name}`, buffer, {
    access: 'public',
  })
  
  return NextResponse.json({ url: blob.url })
}

Document Upload for AI Processing

app/actions.ts
import { put } from "@vercel/blob"

export async function uploadDocument(formData: FormData) {
  const file = formData.get('file') as File
  
  // Validate file type
  const allowedTypes = [
    'application/pdf',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'text/plain',
  ]
  
  if (!allowedTypes.includes(file.type)) {
    throw new Error('Invalid file type. Only PDF, DOCX, and TXT allowed.')
  }
  
  // Upload to Blob
  const blob = await put(`documents/${Date.now()}-${file.name}`, file, {
    access: 'public',
  })
  
  return blob.url
}

Client-Side Upload

React Upload Component

components/FileUpload.tsx
'use client'

import { useState } from 'react'

export function FileUpload() {
  const [uploading, setUploading] = useState(false)
  const [url, setUrl] = useState<string | null>(null)
  
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return
    
    setUploading(true)
    
    const formData = new FormData()
    formData.append('file', file)
    
    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      })
      
      const data = await response.json()
      
      if (data.url) {
        setUrl(data.url)
      }
    } catch (error) {
      console.error('Upload failed:', error)
    } finally {
      setUploading(false)
    }
  }
  
  return (
    <div>
      <input
        type="file"
        onChange={handleUpload}
        disabled={uploading}
      />
      
      {uploading && <p>Uploading...</p>}
      {url && <p>Uploaded: {url}</p>}
    </div>
  )
}

Drag-and-Drop Upload

components/DragDropUpload.tsx
'use client'

import { useState } from 'react'

export function DragDropUpload() {
  const [dragActive, setDragActive] = useState(false)
  
  const handleDrag = (e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    
    if (e.type === 'dragenter' || e.type === 'dragover') {
      setDragActive(true)
    } else if (e.type === 'dragleave') {
      setDragActive(false)
    }
  }
  
  const handleDrop = async (e: React.DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    setDragActive(false)
    
    const file = e.dataTransfer.files?.[0]
    if (!file) return
    
    const formData = new FormData()
    formData.append('file', file)
    
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    })
    
    const data = await response.json()
    console.log('Uploaded:', data.url)
  }
  
  return (
    <div
      onDragEnter={handleDrag}
      onDragLeave={handleDrag}
      onDragOver={handleDrag}
      onDrop={handleDrop}
      className={`border-2 border-dashed p-8 rounded-lg ${
        dragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
      }`}
    >
      <p>Drag and drop file here, or click to select</p>
    </div>
  )
}

Blob Configuration Options

Access Control

const blob = await put(filename, file, {
  access: 'public',  // Anyone with URL can access
})
Use for:
  • Profile pictures
  • Public study materials
  • Shared resources

Additional Options

const blob = await put(filename, file, {
  access: 'public',
  
  // Add custom metadata
  addRandomSuffix: true,  // Prevent filename collisions
  
  // Set content type
  contentType: 'image/png',
  
  // Cache control
  cacheControlMaxAge: 60 * 60 * 24 * 365,  // 1 year
})

File Processing

PDF Processing

Extract text from uploaded PDFs:
import pdfParse from 'pdf-parse'

export async function processPDF(url: string) {
  // Fetch PDF from Blob
  const response = await fetch(url)
  const buffer = await response.arrayBuffer()
  
  // Parse PDF
  const data = await pdfParse(Buffer.from(buffer))
  
  return {
    text: data.text,
    pages: data.numpages,
  }
}

DOCX Processing

import officeParser from 'officeparser'

export async function processDOCX(url: string) {
  const response = await fetch(url)
  const buffer = await response.arrayBuffer()
  
  const text = await officeParser.parseOfficeAsync(Buffer.from(buffer))
  
  return text
}

Image Optimization

import sharp from 'sharp'

export async function optimizeImage(file: File) {
  const buffer = await file.arrayBuffer()
  
  // Resize and optimize
  const optimized = await sharp(Buffer.from(buffer))
    .resize(800, 800, { fit: 'inside' })
    .jpeg({ quality: 80 })
    .toBuffer()
  
  // Upload optimized version
  const blob = await put(`optimized-${file.name}`, optimized, {
    access: 'public',
  })
  
  return blob.url
}

File Management

List Files

import { list } from '@vercel/blob'

export async function listUserFiles(userId: string) {
  const { blobs } = await list({
    prefix: `documents/${userId}/`,
    limit: 100,
  })
  
  return blobs
}

Delete Files

import { del } from '@vercel/blob'

export async function deleteFile(url: string) {
  await del(url)
}

// Delete multiple files
export async function deleteFiles(urls: string[]) {
  await del(urls)
}

Copy Files

import { copy } from '@vercel/blob'

export async function copyFile(fromUrl: string, toPath: string) {
  const blob = await copy(fromUrl, toPath, {
    access: 'public',
  })
  
  return blob.url
}

Security Best Practices

Always validate MIME types:
const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf']

if (!allowedTypes.includes(file.type)) {
  throw new Error('Invalid file type')
}
Enforce reasonable size limits:
const MAX_FILE_SIZE = 10 * 1024 * 1024  // 10MB

if (file.size > MAX_FILE_SIZE) {
  throw new Error('File too large')
}
Remove potentially dangerous characters:
function sanitizeFilename(filename: string): string {
  return filename
    .replace(/[^a-zA-Z0-9._-]/g, '_')
    .replace(/\.\./g, '')
}
Organize files by user:
const path = `users/${userId}/documents/${filename}`
Prevent upload spam:
// Max 10 uploads per hour
const allowed = await checkRateLimit(userId, 10, 3600)

if (!allowed) {
  throw new Error('Upload limit exceeded')
}

Storage Limits

Vercel Blob Pricing

Included:
  • 100GB storage
  • 1GB bandwidth/month
Overage:
  • Storage: $0.15/GB/month
  • Bandwidth: $0.10/GB

Monitoring Usage

Track storage usage in Vercel Dashboard:
  1. Go to Storage tab
  2. Select your Blob store
  3. View Usage metrics:
    • Total files
    • Storage used
    • Bandwidth used

Troubleshooting

Error: UnauthorizedSolutions:
  • Verify BLOB_READ_WRITE_TOKEN is set
  • Check token is correct in environment variables
  • Redeploy after adding token
  • Ensure token has write permissions
Error: Upload exceeds timeoutSolutions:
  • Reduce file size before upload
  • Compress images client-side
  • Increase serverless function timeout (Pro plan)
  • Use chunked uploads for large files
Error: CORS policy blocks accessSolutions:
  • Blob URLs should work cross-origin by default
  • Check browser console for specific error
  • Ensure URL is correct and accessible
Error: Storage limit reachedSolutions:
  • Review and delete old files
  • Implement automatic cleanup of old uploads
  • Upgrade Vercel plan
  • Use alternative storage for large files

Alternative Storage Options

If Vercel Blob doesn’t fit your needs:

AWS S3

Highly scalable, pay-as-you-go pricing

Cloudflare R2

S3-compatible, zero egress fees

Supabase Storage

Integrated with Supabase auth, RLS policies

UploadThing

Developer-friendly, simple integration

Next Steps

PDF Processing

Extract text from uploaded files

AI Generation

Use documents for AI generation

API Reference

View API endpoints

Admin Guide

Learn about security best practices

Build docs developers (and LLMs) love