Cloudinary

Upload straight from the browser to Cloudinary, unsigned with a preset, or signed by your server. Large files upload in resumable chunks.

import { cloudinaryAdapter } from "@uploadcn/core"

Files go directly from the browser to Cloudinary's upload API, with real progress. You don't need the Cloudinary SDK.

Unsigned uploads

Create an unsigned upload preset in the Cloudinary console (Settings → Upload) and restrict it there: allowed formats, max file size, folder, moderation.

const adapter = cloudinaryAdapter({
  cloudName: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME!,
  uploadPreset: "user-uploads",
  folder: "avatars",
})

Anyone can use an unsigned preset

The preset name is public. Use unsigned presets only for low-risk uploads, and lock the preset down. For anything tied to a user, sign uploads on your server.

Signed uploads

Your server decides what's allowed and signs the parameters with your API secret. The secret never reaches the browser.

Add a signing route

app/api/cloudinary/route.ts
import { signCloudinaryParams } from "@uploadcn/server"

export async function POST(request: Request) {
  const session = await auth()
  if (!session) return new Response("Unauthorized", { status: 401 })

  const params = (await request.json()) as Record<string, string>
  // Only sign what this user may upload.
  if (params.folder !== `users/${session.user.id}`) {
    return new Response("Forbidden", { status: 403 })
  }

  return Response.json(
    await signCloudinaryParams(params, {
      apiKey: process.env.CLOUDINARY_API_KEY!,
      apiSecret: process.env.CLOUDINARY_API_SECRET!,
    })
  )
}

Ask it for a signature

const adapter = cloudinaryAdapter({
  cloudName: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME!,
  folder: `users/${userId}`,
  sign: (params) =>
    fetch("/api/cloudinary", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(params),
    }).then((response) => response.json()),
})

Large files

Files larger than chunkSize (default 20 MB, minimum 5 MB) upload in chunks using Cloudinary's chunked upload API. Each finished chunk is saved as resume state, so a retry (or a reload with persistence) continues from the last chunk instead of starting over.

The result

Each successful item's result is Cloudinary's upload response:

onSuccess={(item) => {
  item.result.secure_url // https://res.cloudinary.com/…
  item.result.public_id
  item.result.width
}}

Options

Prop

Type

On this page