# Cloudinary (/docs/storage/cloudinary)



```ts
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 [#unsigned-uploads]

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

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

<Callout type="warn" title="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.
</Callout>

## Signed uploads [#signed-uploads]

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

<Steps>
  <Step>
    ### Add a signing route [#add-a-signing-route]

    ```ts title="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!,
        })
      )
    }
    ```
  </Step>

  <Step>
    ### Ask it for a signature [#ask-it-for-a-signature]

    ```ts
    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()),
    })
    ```
  </Step>
</Steps>

## Large files [#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](/docs/guides/resumable-uploads)) continues from the last
chunk instead of starting over.

## The result [#the-result]

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

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

## Options [#options]

<TypeTable
  type="{
  cloudName: { type: &#x22;string&#x22;, required: true },
  uploadPreset: { type: &#x22;string&#x22;, description: &#x22;An unsigned preset. Use this or `sign`.&#x22; },
  sign: { type: &#x22;(params, item) => Promise<{ signature, timestamp, apiKey }>&#x22;, description: &#x22;Signs `{ ...params, timestamp }` on your server.&#x22; },
  resourceType: { type: '&#x22;auto&#x22; | &#x22;image&#x22; | &#x22;video&#x22; | &#x22;raw&#x22;', default: '&#x22;auto&#x22;' },
  folder: { type: &#x22;string&#x22; },
  params: { type: &#x22;Record<string, string> | (item) => Record<string, string>&#x22;, description: &#x22;Extra upload parameters: tags, context, public_id…&#x22; },
  chunkSize: { type: &#x22;number&#x22;, default: &#x22;20 MB&#x22; },
  apiUrl: { type: &#x22;string&#x22;, default: '&#x22;https://api.cloudinary.com/v1_1&#x22;' },
}"
/>
