Overview
Use Vercel Blob for file storage with preset-based validation and scope-based access control. Files are stored with program/project isolation.
What it is
Vercel Blob storage with abstracted provider pattern, file presets, and permission checking utilities.
Why we use it
Built-in CDN, preset validation (size/type limits), scope-based access control, and soft delete support.
When to use
User file uploads, document storage, image assets, or any binary data that needs to be persisted and accessed.
Key Features
- File presets with size and type validation
- Scope-based access (PUBLIC/PROGRAM/PROJECT)
- Automatic path prefixing by scope
- Soft delete with restore capability
Quick Start
Uploading Files
Basic file upload with preset validation.
// Upload a file to Vercel Blob
import { uploadFile } from '@/features/files';
const result = await uploadFile({
file: formData.get('file') as File,
preset: 'image', // image | document | video | audio
accessLevel: 'PROGRAM', // PUBLIC | PROGRAM | PROJECT
scopeId: programId,
uploadedById: userId,
purpose: 'design',
});
// result = { url, pathname, size, uploadedAt }Patterns
File Presets
Size limits and allowed types by preset.
// File presets with size limits and allowed types
import { FILE_PRESETS } from '@/lib/storage';
FILE_PRESETS.image = {
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
};
FILE_PRESETS.document = {
maxSize: 25 * 1024 * 1024, // 25MB
allowedTypes: ['application/pdf', 'application/msword', ...],
};
FILE_PRESETS.video = {
maxSize: 500 * 1024 * 1024, // 500MB
allowedTypes: ['video/mp4', 'video/webm', 'video/quicktime'],
};
FILE_PRESETS.audio = {
maxSize: 100 * 1024 * 1024, // 100MB
allowedTypes: ['audio/mpeg', 'audio/wav', 'audio/ogg'],
};Access Control
Permission checks for upload and access.
// Access control levels
import { checkUploadPermission, checkFileAccess } from '@/lib/storage';
// Before upload - check permission
const canUpload = await checkUploadPermission({
userId,
accessLevel: 'PROGRAM',
scopeId: programId,
});
if (!canUpload) throw new AuthorizationError('Cannot upload to this program');
// Before download - check access
const canAccess = await checkFileAccess({
fileId,
userId,
});
if (!canAccess) throw new AuthorizationError('Cannot access this file');
// Access rules:
// - PUBLIC: Anyone can access
// - PROGRAM: Must be program member
// - PROJECT: Must be project participant OR program member
// - Uploader always has access to their own files
// - Delete: Uploader OR scope admin (OWNER/BUILDER role)Critical: Always check permissions before upload and access. The uploader always has access to their own files.
Upload API Route
Complete file upload endpoint pattern.
// API route for file upload
import { validateRequest } from '@/lib/validation';
import { handleError } from '@/lib/errors';
import { uploadFile } from '@/features/files';
import { z } from 'zod';
const UploadSchema = z.object({
preset: z.enum(['image', 'document', 'video', 'audio']),
accessLevel: z.enum(['PUBLIC', 'PROGRAM', 'PROJECT']),
scopeId: z.string().optional(),
});
export async function POST(request: Request) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const metadata = JSON.parse(formData.get('metadata') as string);
const validation = validateRequest(metadata, UploadSchema);
if (!validation.success) return validation.error;
const result = await uploadFile({
file,
...validation.data,
uploadedById: auth.user.id,
});
return NextResponse.json({ ok: true, data: result });
} catch (error) {
return handleError(error);
}
}Watch Out
Accepting files without preset validation
Don't
// Accepting any file type
const file = formData.get('file');
await uploadToBlob(file); // No validation!Do
// Validate against preset
import { validateFile } from '@/lib/storage';
const file = formData.get('file') as File;
const validation = validateFile(file, 'image');
if (!validation.valid) {
throw new ValidationError(validation.error);
}
await uploadFile({ file, preset: 'image', ... });- Missing permission checks on upload/download
- Not enforcing file size limits
- Making files public when they should be scoped