File Uploads & Storage

Handle file uploads and object storage.

The template includes one authenticated image upload route. It writes files to the local filesystem and returns public URLs — a working starting point, but not durable storage. Treat it as the integration point for S3, R2, or Vercel Blob.

What it provides

  • An authenticated POST /api/storage/upload-image route.
  • Image-only validation and collision-free filenames.
  • Returned /uploads/... URLs for the saved files.

Important files

FileRole
src/app/api/storage/upload-image/route.tsUpload handler
public/uploads/Where files are written (created at runtime)

How the route works

src/app/api/storage/upload-image/route.ts
export async function POST(request: Request) {
  const session = await auth.api.getSession({ headers: await headers() });
  if (!session?.user) return /* 401 Unauthorized */;

  const formData = await request.formData();
  const files = formData.getAll("files");
  // …writes each file, returns its URL
}

Step by step:

  1. Auth required — the route reads the Better Auth session and returns 401 if there's no user.
  2. Multipart form — it reads the files field (getAll, so multiple files are supported) and rejects an empty upload with 400.
  3. Image-only — each entry must be a File whose type starts with image/; anything else is rejected.
  4. Safe filenames — the extension is mapped from the MIME type (jpg/png/webp/gif), and the name is a crypto.randomUUID() so uploads never collide or expose the original name.
  5. Write + respond — files are written to public/uploads/, and the route returns { success: true, data: { urls: ["/uploads/<uuid>.<ext>"] } }.

The durability problem

Local disk is not durable in production. Files are written to public/uploads, which works in local dev but does not persist on Vercel, serverless, or containerized deploys — the filesystem is ephemeral and per-instance. Uploads will vanish on redeploy/scale and won't be shared across instances. Do not rely on this route in production as-is.

Integrating object storage

Swap the filesystem write for an object-storage SDK (AWS S3, Cloudflare R2, Vercel Blob, …). Keep the route's auth check and validation; replace only the "write file + build URL" part:

Add the provider SDK + credentials

Install the SDK and add its credentials as server-only env vars (see Environment Variables).

Replace the write

Instead of writeFile(...) to public/uploads, upload the buffer to your bucket and use the returned/object URL.

Return the real URL

Push the bucket/CDN URL into the urls array the route already returns, so callers need no changes.

Production requirements

Before exposing uploads to real users, add:

  • Size limits — reject oversized files (the route does not cap size).
  • Stronger validation — verify real content type, not just the declared MIME, and consider re-encoding images.
  • Access policy — decide public vs. signed/private URLs per object.
  • Cleanup — delete orphaned files when records are removed.
  • CDN — serve through a CDN for performance.
  • Abuse protection — rate-limit the route and scope it to the right users.

On this page