Supercharging File Hosting: Why I Migrated from Supabase to Vercel Blob Storage
As a developer building an audio recording and processing application, I faced a critical infrastructure decision that many of us encounter: choosing the right file storage solution. My app needed to handle large audio files from call recordings, process them for transcription, and ensure secure access. While Supabase had served me well for databases and auth, its storage limitations were becoming a bottleneck. Here's how Vercel Blob storage transformed my application's capabilities.
The Storage Dilemma
When you're building apps that handle substantial user uploads - in my case, audio recordings often exceeding several megabytes - every limitation becomes a potential showstopper. I hit these hurdles head-on with Supabase:
- A strict 500MB storage limit on the free tier
- Maximum file size restriction of 50MB
- Separate authentication system for file access
- Infrastructure management split between Vercel and Supabase
Here's what my initial Supabase setup looked like:
// Initial Supabase configuration
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
)
// Upload endpoint for audio files
async function uploadRecording(req, res) {
const { data, error } = await supabase
.storage
.from('recordings')
.upload(`calls/${userId}/${fileName}`, audioFile, {
cacheControl: '3600',
upsert: false
})
if (error) {
return res.status(500).json({ error: error.message })
}
return res.status(200).json({ path: data.path })
}
Enter Vercel Blob Storage
When Vercel announced their Blob storage solution, several features caught my attention:
- 5GB free tier storage (10x more than Supabase)
- 50MB file size limit on Pro tier (perfect for audio files)
- 2 million monthly storage requests included
- Seamless integration with existing Vercel deployments
- Built-in CDN performance
- Unified authentication system
The Migration Experience
The switch involved several key steps. Here's how I handled each one:
1. Updating Upload Functionality
import { put, del, list } from '@vercel/blob';
// New upload endpoint
async function uploadRecording(req, res) {
const blob = await request.formData();
const audioFile = blob.get("file");
try {
const { url } = await put(
`calls/${userId}/${Date.now()}_${audioFile.name}`,
audioFile.stream(),
{ access: 'private' }
);
return res.status(200).json({ url });
} catch (error) {
return res.status(500).json({ error: error.message });
}
}
2. Migrating Existing Files
I created a migration script to handle the transfer of existing files and database records:
import { createClient } from '@supabase/supabase-js';
import { getBlob } from '@vercel/blob';
import { PrismaClient } from '@prisma/client';
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);
const prisma = new PrismaClient();
const blob = await getBlob();
async function migrateFiles() {
try {
// Query database for all recordings that need migration
const recordingsToMigrate = await prisma.recording.findMany({
where: {
fileUrl: {
contains: 'supabase'
}
},
select: {
id: true,
fileUrl: true,
fileName: true,
userId: true,
metadata: true
}
});
console.log(`Found ${recordingsToMigrate.length} recordings to migrate`);
// Iterate through each recording for migration
for (const recording of recordingsToMigrate) {
try {
// Get filename from either stored filename or extract from URL
const fileName = recording.fileName || recording.fileUrl.split('/').pop();
// Download the file from Supabase storage
const { data, error: downloadError } = await supabase.storage
.from('recordings')
.download(fileName);
if (downloadError) {
console.error(`Error downloading ${fileName}:`, downloadError);
continue;
}
// Create organized file path and upload to Vercel Blob
const newPath = `recordings/${recording.userId}/${fileName}`;
const { url: vercelUrl } = await blob.put(newPath, data, {
access: 'private',
addRandomSuffix: false,
contentType: recording.metadata?.mimeType || 'audio/wav'
});
// Update the database record with the new Vercel Blob URL
await prisma.recording.update({
where: {
id: recording.id
},
data: {
fileUrl: vercelUrl,
storageProvider: 'VERCEL_BLOB',
updatedAt: new Date(),
metadata: {
...recording.metadata,
migratedAt: new Date().toISOString(),
previousUrl: recording.fileUrl
}
}
});
console.log(`Successfully migrated recording ${recording.id}`);
} catch (error) {
console.error(`Failed to migrate recording ${recording.id}:`, error);
// Store failed migration information for tracking
await prisma.migrationLog.create({
data: {
recordingId: recording.id,
error: error.message,
status: 'FAILED'
}
});
}
}
// Generate migration statistics
const migrationSummary = await prisma.recording.groupBy({
by: ['storageProvider'],
_count: {
id: true
}
});
console.log('Migration Summary:', migrationSummary);
} catch (error) {
console.error('Migration failed:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
// Main migration function with error handling
async function executeMigration() {
try {
await migrateFiles();
console.log('Migration completed successfully');
} catch (error) {
console.error('Migration failed:', error);
await notifyAdmins('Migration failed: ' + error.message);
}
}
// Function to handle retrying failed migrations
async function retryFailedMigrations() {
const failedMigrations = await prisma.migrationLog.findMany({
where: {
status: 'FAILED'
},
select: {
recordingId: true
}
});
for (const failed of failedMigrations) {
console.log(`Retrying migration for recording ${failed.recordingId}`);
// Implement retry logic here
}
}
// Start the migration process
executeMigration();
3. Updating Frontend Components
Here's how I handle file uploads in the React frontend:
function AudioRecorder() {
async function handleRecordingUpload(audioBlob) {
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
try {
const response = await fetch('/api/upload-recording', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
// Store URL and trigger transcription process
} catch (error) {
console.error('Upload failed:', error);
}
}
return (
<div className="recording-interface">
<RecordButton onRecordingComplete={handleRecordingUpload} />
<TranscriptionStatus />
</div>
);
}
The Results
The migration brought immediate benefits:
- Performance Boost: Audio file loading times improved by 40% thanks to Vercel's CDN
- Cost Savings: Stayed within free tier limits despite growing user base
- Simplified Security: One authentication system for both app and file access
- Better Developer Experience: Single dashboard for monitoring and debugging
- Improved User Experience: Higher file size limits meant fewer failed uploads
Making the Move: Your Action Plan
If you're considering a similar switch, here's what I recommend:
- Start with a test migration of a small subset of files
- Monitor performance metrics before and after
- Update your database schema to handle new URL formats
- Implement proper error handling for the migration process
- Consider using Vercel's monitoring tools to track performance
For developers building apps that handle large files, especially media files like audio recordings, Vercel Blob storage offers a compelling solution. The combination of generous limits, integrated security, and simplified infrastructure makes it worth considering.