Skip to content
← All articles

Aug 16, 2026
1 min read

  • Next.js
  • Cloudinary
  • Serverless
  • Security
  • Frontend

Serverless functions cap request bodies at a few megabytes, and phone photos are bigger. Signed direct uploads fix it without exposing your secret.

On my own portfolio, the admin let me attach several images to a project. It worked locally — and failed in production as soon as I picked real screenshots. The reason is common enough to be worth writing down.

The problem: your API route is a bottleneck

The first version did the obvious thing:

  1. the browser sends the file to /api/upload;
  2. the route forwards it to Cloudinary with the secret key;
  3. it returns the URL.

On serverless platforms, the request body of a function is capped (on Vercel, around 4.5 MB). A modern phone photo or a retina screenshot easily exceeds that. Uploading ten images one by one through the function is also slow.

The fix: let the browser upload directly, with a signature

The secret must stay on the server — but the file doesn't need to pass through it. The server only signs the upload parameters:

// app/api/admin/uploads/sign/route.ts — protected by the admin auth
export async function POST() {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const folder = 'portfolio/projects';
  const signature = createHash('sha1')
    .update(`folder=${folder}&timestamp=${timestamp}${process.env.CLOUDINARY_API_SECRET}`)
    .digest('hex');
  return Response.json({ timestamp, folder, signature, apiKey: process.env.CLOUDINARY_API_KEY });
}

The browser then posts the file straight to Cloudinary with those fields. The signature only allows that folder, for a limited time, so it can't be reused to upload anywhere else.

Progress bars for free

fetch can't report upload progress yet, but XMLHttpRequest can:

xhr.upload.onprogress = (e) => e.lengthComputable && onProgress(e.loaded / e.total);

With a small pool of three uploads in parallel, ten images feel fast, and each one shows its own progress bar.

Guard rails that still matter

Going direct doesn't remove validation; it moves it:

  • In the browser: accepted types, maximum size, maximum number of files — for fast feedback.
  • On the signing route: only authenticated admins get a signature, and only for known folders.
  • On save: the API that stores the project validates the URLs and caps the gallery size, because the browser can't be trusted.

Result

Uploads no longer depend on the function's body limit, they run in parallel, and the secret never leaves the server. The same pattern works with S3 presigned URLs or any storage that supports signed uploads.