File Upload Adapters
Configure file uploads with Vercel Blob, Cloudflare R2, AWS S3, or UploadThing
NowStack comes with a flexible file upload system using adapters. You can easily switch between different storage providers without changing your application code.
How It Works
The upload system uses an adapter pattern defined in src/lib/files/upload-file.ts:
export type UploadFileAdapter = {
uploadFile: (params: {
file: File;
path: string;
}) => Promise<
{ error: null; data: { url: string } } | { error: Error; data: null }
>;
uploadFiles: (
params: { file: File; path: string }[],
) => Promise<{ error: Error | null; data: { url: string } | null }[]>;
};The active upload implementation lives in convex/files/actions.ts.
Available Adapters
Vercel Blob (Default)
Vercel Blob is the default and recommended option. It's automatically configured when deploying to Vercel.
Setup:
- Go to your Vercel Dashboard > Storage > Create Database > Blob
- Connect it to your project
- The
BLOB_READ_WRITE_TOKENis automatically added to your environment
For local development:
- Go to Vercel Dashboard > Storage > Blob > Tokens
- Copy the token to your
.envfile:
BLOB_READ_WRITE_TOKEN="vercel_blob_..."Adapter code (already included at src/lib/files/vercel-blob-adapter.ts):
import { put } from "@vercel/blob";
import type { UploadFileAdapter } from "./upload-file";
export const fileAdapter: UploadFileAdapter = {
uploadFile: async (params) => {
try {
const blob = await put(params.file.name, params.file, {
access: "public",
});
return { error: null, data: { url: blob.url } };
} catch (error) {
return {
error:
error instanceof Error ? error : new Error("Failed to upload file"),
data: null,
};
}
},
uploadFiles: async (params) => {
const promises = params.map(async (param) => {
try {
const blob = await put(param.file.name, param.file, {
access: "public",
});
return { error: null, data: { url: blob.url } };
} catch (error) {
return {
error:
error instanceof Error ? error : new Error("Failed to upload file"),
data: null,
};
}
});
return Promise.all(promises);
},
};Cloudflare R2 / AWS S3
Use Cloudflare R2 or AWS S3 for more control over your storage or to avoid vendor lock-in.
Setup:
- Install dependencies:
pnpm add @aws-sdk/client-s3 mime-types
pnpm add -D @types/mime-types- Add environment variables to
.env:
AWS_ENDPOINT="https://your-account-id.r2.cloudflarestorage.com"
AWS_ACCESS_KEY_ID="your-access-key"
AWS_SECRET_ACCESS_KEY="your-secret-key"
AWS_S3_BUCKET_NAME="your-bucket-name"
R2_URL="https://your-public-bucket-url.com"- Create the adapter at
src/lib/files/r2-adapter.ts:
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import type { UploadFileAdapter } from "./upload-file";
const s3 = new S3Client({
region: "auto",
endpoint: process.env.AWS_ENDPOINT,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export const fileAdapter: UploadFileAdapter = {
uploadFile: async (params) => {
try {
const fileBuffer = await params.file.arrayBuffer();
const buffer = Buffer.from(fileBuffer);
const uniqueFileName = `${params.path}/${Date.now()}-${params.file.name}`;
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: uniqueFileName,
Body: buffer,
ContentType: params.file.type,
});
await s3.send(command);
const url = `${process.env.R2_URL}/${uniqueFileName}`;
return { error: null, data: { url } };
} catch (error) {
return {
error: error instanceof Error ? error : new Error("Upload failed"),
data: null,
};
}
},
uploadFiles: async (params) => {
const results = await Promise.allSettled(
params.map((param) => fileAdapter.uploadFile(param)),
);
return results.map((result) => {
if (result.status === "fulfilled") {
return result.value;
}
return {
error: new Error(result.reason?.message || "Upload failed"),
data: null,
};
});
},
};- Update the upload implementation in
convex/files/actions.ts:
// Change this:
import { fileAdapter } from "@/lib/files/vercel-blob-adapter";
// To this:
import { fileAdapter } from "@/lib/files/r2-adapter";