better-auth-cloudflare provides seamless file upload, tracking, and management with Cloudflare R2 object storage. Files are automatically tracked in your database with rich metadata and custom fields.
import { withCloudflare } from "better-auth-cloudflare";
export const auth = betterAuth(
withCloudflare(
{
r2: {
bucket: getCloudflareContext().env.R2_BUCKET,
maxFileSize: 2 * 1024 * 1024, // 2MB
allowedTypes: [".jpg", ".jpeg", ".png", ".gif"],
additionalFields: {
category: { type: "string", required: false },
isPublic: { type: "boolean", required: false },
description: { type: "string", required: false },
},
hooks: {
upload: {
before: async (file, ctx) => {
// Only allow authenticated users to upload files
if (ctx.session === null) {
return null; // Blocks upload
}
// Only allow paid users to upload files (for example)
const isPaidUser = (userId: string) => true; // example
if (isPaidUser(ctx.session.user.id) === false) {
return null; // Blocks upload
}
// Allow upload
},
after: async (file, ctx) => {
// Track your analytics (for example)
console.log("File uploaded:", file);
},
},
download: {
before: async (file, ctx) => {
// Only allow user to access their own files (by default all files are public)
if (file.isPublic === false && file.userId !== ctx.session?.user.id) {
return null; // Blocks download
}
// Allow download
},
},
},
},
// ... other config
},
{
// ... your auth config
}
)
);import { createAuthClient } from "better-auth/client";
import { cloudflareClient } from "better-auth-cloudflare/client";
const authClient = createAuthClient({
baseURL: "/api/auth", // Adjust if your auth routes are elsewhere
plugins: [cloudflareClient()], // Enables uploadFile method and files endpoints
});
export default authClient;Note: The cloudflareClient() plugin adds the uploadFile convenience method as well as inferred API client methods for file management (files.list, files.download, files.delete, files.get).
Track additional metadata with type-safe custom fields:
const r2Config = {
bucket: env.R2_BUCKET,
additionalFields: {
category: { type: "string", required: false },
isPublic: { type: "boolean", required: false },
description: { type: "string", required: false },
// Additional examples...
priority: { type: "number", required: false },
tags: { type: "string[]", required: false },
},
} satisfies R2Config;Use uploadFile method. Filename and content type are automatically inferred from the File object:
import authClient from "@/lib/authClient";
// Upload with custom fields
const file = fileInput.files[0];
const category = "documents";
const description = "Important contract document";
const isPublic = false;
const result = await authClient.uploadFile(file, {
isPublic,
...(category.trim() && { category: category.trim() }),
...(description.trim() && { description: description.trim() }),
});
if (result.error) {
console.error("Upload failed:", result.error.message || "Failed to upload file. Please try again.");
} else {
console.log("File uploaded:", result.data);
// Clear form or refresh file list as needed
}Hooks let you add business logic at key points in the file lifecycle. Only define the hooks you need - much cleaner than individual callbacks:
const r2Config = {
bucket: env.R2_BUCKET,
additionalFields: {
category: { type: "string", required: false },
isPublic: { type: "boolean", required: false },
description: { type: "string", required: false },
},
hooks: {
// Upload lifecycle
upload: {
before: async (file, ctx) => {
// Only allow authenticated users to upload files
if (ctx.session === null) {
return null; // Blocks upload
}
// Only allow paid users to upload files (for example)
const isPaidUser = (userId: string) => true; // example
if (isPaidUser(ctx.session.user.id) === false) {
return null; // Blocks upload
}
// Allow upload
},
after: async (file, ctx) => {
// Track your analytics (for example)
console.log("File uploaded:", file);
},
},
// Download lifecycle
download: {
before: async (file, ctx) => {
// Only allow user to access their own files (by default all files are public)
if (file.isPublic === false && file.userId !== ctx.session?.user.id) {
return null; // Blocks download
}
// Allow download
},
},
// Delete lifecycle
delete: {
before: async (file, ctx) => {
// Remove from search index
await removeFromSearchIndex(file.id);
},
},
},
};Hooks:
upload- Before and after upload hooksdownload- Before and after download hooksdelete- Before and after delete hooks
// Get files for current user
const result = await authClient.files.list();
if (result.data) {
const { files, nextCursor, hasMore } = result.data;
console.log("User files:", files);
// Load next page if available
if (hasMore && nextCursor) {
const nextPage = await authClient.files.list({
limit: 20,
cursor: nextCursor,
});
console.log("Next page:", nextPage.data?.files);
}
}const downloadFile = async (fileId: string, filename: string) => {
try {
const result = await authClient.files.download({ fileId });
if (result.error) {
console.error("Download failed:", result.error);
return;
}
// Extract blob from Better Auth response structure
const response = result.data;
const blob = response instanceof Response ? await response.blob() : response;
// Create and trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
// Cleanup
setTimeout(() => {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}, 100);
} catch (error) {
console.error("Failed to download file:", error);
}
};const deleteFile = async (fileId: string) => {
try {
const result = await authClient.files.delete({ fileId });
if (!result.error) {
console.log("File deleted successfully");
// Refresh your file list here if needed
} else {
console.error("Delete failed:", result.error);
}
} catch (error) {
console.error("Failed to delete file:", error);
}
};// Get file metadata by ID
const result = await authClient.files.get({ fileId });
if (result.data) {
console.log("File metadata:", result.data);
}Use these type-safe client methods:
| Client Method | Description |
|---|---|
authClient.uploadFile(file, metadata) |
Upload files with custom fields |
authClient.files.list({ limit?, cursor? }) |
List user's files with pagination |
authClient.files.download({ fileId }) |
Download file |
authClient.files.delete({ fileId }) |
Delete file |
authClient.files.get({ fileId }) |
Get file metadata |
These are the underlying endpoints (use client methods above instead):
| Endpoint | Method | Description | Parameters |
|---|---|---|---|
/files/upload-raw |
POST | Upload file | Body: File (as binary), Headers: x-filename, x-file-metadata (JSON) |
/files/list |
GET | List user's files | Query: limit?: number, cursor?: string Response: { files: File[], nextCursor: string|null, hasMore: boolean } |
/files/download |
POST | Download file | Body: { fileId: string } Response: File as binary data |
/files/delete |
POST | Delete file | Body: { fileId: string } |
/files/get |
POST | Get file metadata | Body: { fileId: string } Response: { data: FileMetadata } |