File Upload Adapters
Configure file uploads with Vercel Blob, Cloudflare R2, AWS S3, or UploadThing
BAPTCOM.TS 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 adapter is imported in src/features/images/upload-image.action.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 import in
src/features/images/upload-image.action.ts:
// Change this:
import { fileAdapter } from "@/lib/files/vercel-blob-adapter";
// To this:
import { fileAdapter } from "@/lib/files/r2-adapter";
Video tutorials:
UploadThing
UploadThing is a developer-friendly file upload service with a generous free tier.
Setup:
- Install the package:
pnpm add uploadthing
- Add your token to
.env:
UPLOADTHING_TOKEN="your-uploadthing-token"
- Create the adapter at
src/lib/files/uploadthing-adapter.ts:
import { UTApi } from "uploadthing/server";
import type { UploadFileAdapter } from "./upload-file";
export const utapi = new UTApi({});
export const fileAdapter: UploadFileAdapter = {
uploadFile: async (params) => {
const response = await utapi.uploadFiles([params.file]);
if (response[0].error) {
return { error: new Error(response[0].error.message), data: null };
}
return { error: null, data: { url: response[0].data.ufsUrl } };
},
uploadFiles: async (params) => {
const response = await utapi.uploadFiles(params.map((param) => param.file));
return response.map((res) => {
if (res.error) {
return { error: new Error(res.error.message), data: null };
}
return { error: null, data: { url: res.data.ufsUrl } };
});
},
};
- Update the import in
src/features/images/upload-image.action.ts:
import { fileAdapter } from "@/lib/files/uploadthing-adapter";
Switching Adapters
To switch between adapters, simply change the import in src/features/images/upload-image.action.ts:
// Vercel Blob (default)
import { fileAdapter } from "@/lib/files/vercel-blob-adapter";
// Cloudflare R2 / AWS S3
import { fileAdapter } from "@/lib/files/r2-adapter";
// UploadThing
import { fileAdapter } from "@/lib/files/uploadthing-adapter";
Enabling Image Upload
By default, image upload is disabled. To enable it:
- Open
src/site-config.ts - Set
enableImageUploadtotrue:
export const SiteConfig = {
// ...
features: {
enableImageUpload: true,
// ...
},
};
This enables drag-and-drop and click-to-upload functionality throughout the app.