Features
File Uploads
Secure file uploads with UploadThing integration
File Uploads
The starter kit uses UploadThing for secure, authenticated file uploads.
Features
- ✅ Authenticated Uploads - Only signed-in users can upload
- ✅ File Type Validation - Restrict by MIME type
- ✅ Size Limits - Configurable max file size
- ✅ Image Optimization - Automatic image compression
- ✅ CDN Delivery - Fast global content delivery
- ✅ User Tracking - Audit who uploaded what
- ✅ Multiple Upload Endpoints - Different limits per use case
Setup
1. Create UploadThing Account
Sign up at uploadthing.com.
2. Get API Token
- Go to Dashboard
- Create an app
- Copy the token
3. Configure Environment
UPLOADTHING_TOKEN="eyJhcHBJZCI6xxxxxxxxxxxx"That's it! The integration is ready.
Upload Endpoints
Avatar Upload
For user profile pictures:
// src/app/api/uploadthing/route.ts
export const ourFileRouter = {
avatarUpload: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.user) throw new Error("Unauthorized");
return { userId: session.user.id };
})
.onUploadComplete(async ({ file, metadata }) => {
console.log(`Avatar uploaded by ${metadata.userId}:`, file.url);
// Update user profile with new avatar
await db
.update(user)
.set({ image: file.url })
.where(eq(user.id, metadata.userId));
return { url: file.url };
}),
};Configuration:
- Max size: 4MB
- Max files: 1
- Allowed: Images only (jpg, png, webp, gif)
Blog Image Upload
For blog post images:
blogImageUpload: f({ image: { maxFileSize: "8MB", maxFileCount: 10 } })
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.user?.role !== "admin") {
throw new Error("Unauthorized");
}
return { userId: session.user.id, folder: "blog" };
})
.onUploadComplete(async ({ file, metadata }) => {
console.log(`Blog image uploaded:`, file.url);
return { url: file.url };
}),Configuration:
- Max size: 8MB
- Max files: 10
- Allowed: Images only
- Admin only
Document Upload
For PDF and document uploads:
documentUpload: f({ pdf: { maxFileSize: "16MB", maxFileCount: 5 } })
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.user) throw new Error("Unauthorized");
return { userId: session.user.id };
})
.onUploadComplete(async ({ file, metadata }) => {
// Log upload for audit
await logSecurityEvent({
eventType: "file_uploaded",
userId: metadata.userId,
metadata: { fileName: file.name, fileUrl: file.url },
});
return { url: file.url };
}),Configuration:
- Max size: 16MB
- Max files: 5
- Allowed: PDF only
Client-Side Upload
React Component
"use client";
import { UploadButton, UploadDropzone } from "@/lib/uploadthing";
export function ImageUploader() {
return (
<UploadButton
endpoint="avatarUpload"
onClientUploadComplete={(res) => {
console.log("Files uploaded:", res);
toast.success("Upload complete!");
}}
onUploadError={(error: Error) => {
toast.error(`Upload failed: ${error.message}`);
}}
/>
);
}
// Or use dropzone
export function ImageDropzone() {
return (
<UploadDropzone
endpoint="blogImageUpload"
onClientUploadComplete={(res) => {
console.log("Uploaded:", res);
}}
onUploadError={(error) => {
console.error("Error:", error);
}}
/>
);
}Custom Upload UI
"use client";
import { useUploadThing } from "@/lib/uploadthing";
import { useState } from "react";
export function CustomUploader() {
const [files, setFiles] = useState<File[]>([]);
const { startUpload, isUploading } = useUploadThing("avatarUpload");
const handleUpload = async () => {
if (!files.length) return;
const uploadedFiles = await startUpload(files);
if (uploadedFiles) {
console.log("Uploaded:", uploadedFiles);
toast.success("Files uploaded successfully!");
}
};
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => {
const fileList = e.target.files;
if (fileList) {
setFiles(Array.from(fileList));
}
}}
/>
<Button onClick={handleUpload} disabled={isUploading}>
{isUploading ? "Uploading..." : "Upload"}
</Button>
</div>
);
}Security
Authentication Required
All uploads require authentication:
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.user) {
throw new Error("Unauthorized");
}
return { userId: session.user.id };
})Role-Based Access
Restrict uploads by role:
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
if (session?.user?.role !== "admin") {
throw new Error("Admin access required");
}
return { userId: session.user.id };
})File Type Validation
Specified in endpoint configuration:
// Only images
f({ image: { ... } })
// Only PDFs
f({ pdf: { ... } })
// Only videos
f({ video: { ... } })
// Any file (use sparingly)
f({ blob: { ... } })Size Limits
Per-endpoint size limits:
f({
image: {
maxFileSize: "4MB", // 4 megabytes
maxFileCount: 5, // Max 5 files at once
},
});Audit Logging
Track all uploads:
.onUploadComplete(async ({ file, metadata }) => {
await logSecurityEvent({
eventType: "file_uploaded",
userId: metadata.userId,
severity: "low",
description: `File uploaded: ${file.name}`,
metadata: {
fileName: file.name,
fileSize: file.size,
fileUrl: file.url,
fileType: file.type,
},
});
})Image Optimization
UploadThing automatically optimizes images:
- Compression: Reduces file size
- Format conversion: Converts to WebP when beneficial
- Responsive images: Multiple sizes available
Accessing Optimized Images
// Original
const url = "https://utfs.io/f/abc123.jpg";
// Optimized (800px width)
const optimized = `${url}?w=800`;
// With quality
const quality = `${url}?w=800&q=80`;
// WebP format
const webp = `${url}?w=800&fmt=webp`;Using in Next.js Image
import Image from "next/image";
<Image
src={file.url}
alt="Uploaded image"
width={800}
height={600}
// UploadThing URLs work with Next.js Image
/>;File Management
Listing User Files
// Track uploads in database
const userUploads = pgTable("user_uploads", {
id: serial("id").primaryKey(),
userId: text("user_id").references(() => user.id),
fileName: text("file_name"),
fileUrl: text("file_url"),
fileSize: integer("file_size"),
fileType: text("file_type"),
uploadedAt: timestamp("uploaded_at").defaultNow(),
});
// Save on upload
.onUploadComplete(async ({ file, metadata }) => {
await db.insert(userUploads).values({
userId: metadata.userId,
fileName: file.name,
fileUrl: file.url,
fileSize: file.size,
fileType: file.type,
});
})Deleting Files
import { UTApi } from "uploadthing/server";
const utapi = new UTApi();
// Delete file
await utapi.deleteFiles(["file-key"]);
// Or multiple
await utapi.deleteFiles(["key1", "key2", "key3"]);Advanced Usage
Progress Tracking
"use client";
import { useUploadThing } from "@/lib/uploadthing";
export function ProgressUploader() {
const [progress, setProgress] = useState(0);
const { startUpload } = useUploadThing("avatarUpload", {
onUploadProgress: (p) => {
setProgress(p);
},
});
return (
<div>
{progress > 0 && <Progress value={progress} />}
{/* Upload UI */}
</div>
);
}Metadata
Pass custom metadata:
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
return {
userId: session.user.id,
uploadedAt: new Date().toISOString(),
source: "blog-editor",
// Custom metadata
};
})Multiple Endpoints
Create different endpoints for different use cases:
export const ourFileRouter = {
avatarUpload: f({ image: { maxFileSize: "4MB" } })...,
blogImageUpload: f({ image: { maxFileSize: "8MB" } })...,
documentUpload: f({ pdf: { maxFileSize: "16MB" } })...,
videoUpload: f({ video: { maxFileSize: "64MB" } })...,
}Best Practices
- Always authenticate - Never allow anonymous uploads
- Validate file types - Restrict to needed types
- Set size limits - Prevent abuse and cost overruns
- Log uploads - Track who uploaded what
- Clean up unused files - Delete files when no longer needed
- Use appropriate limits - Don't allow 100MB uploads if 4MB suffices
- Handle errors gracefully - Show user-friendly error messages
Troubleshooting
Upload fails immediately
- Check
UPLOADTHING_TOKENis set - Verify token is valid
- Ensure user is authenticated
"Unauthorized" error
Check middleware authentication:
.middleware(async ({ req }) => {
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.user) throw new Error("Unauthorized");
return { userId: session.user.id };
})File too large
- Check client file size before upload
- Increase
maxFileSizeif appropriate - Show user the size limit
Upload stuck at 100%
- Check
onUploadCompletedoesn't have errors - Ensure database writes succeed
- Check server logs
Pricing
UploadThing pricing (as of 2024):
- Free Tier: 2GB storage + 2GB bandwidth
- Pro: $10/month for 100GB storage + 100GB bandwidth
- Pay as you go: Additional usage charged