|
1 | 1 | # Cloudinary React SDK Patterns & Common Errors |
2 | 2 |
|
| 3 | +**Scope**: These rules apply to **React (web)** with Vite and the browser Upload Widget. For **React Native** uploads (including signed upload), see: https://cloudinary.com/documentation/react_native_image_and_video_upload#signed_upload — same “never expose secret, generate signature on backend” principle, but React Native uses the `upload()` method and backend SDKs differently. |
| 4 | + |
3 | 5 | ## Official Documentation |
4 | 6 | - **Transformation Rules**: https://cloudinary.com/documentation/cloudinary_transformation_rules.md |
5 | 7 | - **Transformation Reference**: https://cloudinary.com/documentation/transformation_reference |
6 | 8 | - **React Image Transformations & Plugins**: https://cloudinary.com/documentation/react_image_transformations#plugins |
7 | 9 | - **React Video Transformations**: https://cloudinary.com/documentation/react_video_transformations |
8 | 10 | - **Cloudinary Video Player** (standalone player): https://cloudinary.com/documentation/cloudinary_video_player |
9 | 11 | - **Video Player React Tutorial**: https://cloudinary.com/documentation/video_player_react_tutorial#banner |
| 12 | +- **Upload Widget (signed uploads)**: https://cloudinary.com/documentation/upload_widget#signed_uploads |
| 13 | +- **Upload assets in Next.js (backend signature)**: https://cloudinary.com/documentation/upload_assets_in_nextjs_tutorial |
| 14 | +- **Cloudinary Node.js SDK (server-side signing)** — use **v2**: `import { v2 as cloudinary } from 'cloudinary'`; do not use v1 (e.g. 1.47.0). https://cloudinary.com/documentation/node_integration |
| 15 | +- **React Native image and video upload (signed)**: https://cloudinary.com/documentation/react_native_image_and_video_upload#signed_upload |
10 | 16 | - Always consult the official transformation rules when creating transformations |
11 | 17 | - Use only officially supported parameters from the transformation reference |
12 | 18 |
|
|
21 | 27 | - Always restart dev server after adding/updating `.env` variables |
22 | 28 |
|
23 | 29 | ## Upload Presets |
24 | | -- **Unsigned upload presets are required for client-side uploads** - Transformations work without them, but uploads need an unsigned preset |
25 | | -- ✅ Create unsigned upload preset: https://console.cloudinary.com/app/settings/upload/presets |
| 30 | +- **Unsigned** = client-only uploads (no backend). **Signed** = backend required, more secure. See **"Signed vs unsigned uploads"** below for when to use which. |
| 31 | +- ✅ Create unsigned upload preset (for simple client uploads): https://console.cloudinary.com/app/settings/upload/presets |
26 | 32 | - ✅ Set preset in `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name` |
27 | 33 | - ✅ Use in code: `import { uploadPreset } from './cloudinary/config'` |
28 | 34 | - ⚠️ If upload preset is missing, the Upload Widget will show an error message |
29 | 35 | - ⚠️ Upload presets must be set to "Unsigned" mode for client-side usage (no API key/secret needed) |
| 36 | +- **When unsigned upload fails**: First check that the user configured their upload preset: |
| 37 | + 1. Is `VITE_CLOUDINARY_UPLOAD_PRESET` set in `.env`? (must match preset name exactly) |
| 38 | + 2. Does the preset exist in the dashboard under Settings → Upload → Upload presets? |
| 39 | + 3. Is the preset set to **Unsigned** (not Signed)? |
| 40 | + 4. Was the dev server restarted after adding/updating `.env`? |
30 | 41 |
|
31 | 42 | ## Import Patterns |
32 | 43 | - ✅ Import Cloudinary instance: `import { cld } from './cloudinary/config'` |
|
132 | 143 | ``` |
133 | 144 | - ✅ Upload result contains: `public_id`, `secure_url`, `width`, `height`, etc. |
134 | 145 |
|
| 146 | +## Signed vs unsigned uploads (when to use which) |
| 147 | + |
| 148 | +**Unsigned uploads** (simpler, no backend required): |
| 149 | +- Use when: Quick prototypes, low-risk apps, or when anyone with the preset name may upload. |
| 150 | +- Preset: Create an **Unsigned** upload preset in Cloudinary dashboard (Settings → Upload → Upload presets). Put preset name in `.env` as `VITE_CLOUDINARY_UPLOAD_PRESET`. |
| 151 | +- Client: Widget needs only `cloudName` and `uploadPreset`. No API key or secret; no backend. |
| 152 | +- Trade-off: Anyone who knows the preset name can upload. Use only when that is acceptable. |
| 153 | + |
| 154 | +**Signed uploads** (more secure, backend required): |
| 155 | +- Use when: Production apps, authenticated users, or when you need to control who can upload. |
| 156 | +- Preset: Create a **Signed** upload preset in the dashboard. The backend generates a signature using your API secret; the client never sees the secret. |
| 157 | +- Client: Widget gets `api_key` (from your backend), `uploadPreset`, and an `uploadSignature` **function** that calls your backend for each upload. API secret stays on server only. |
| 158 | +- Trade-off: Requires a backend (Node/Express, Next.js API route, etc.) to sign requests. More secure; signature validates each upload. |
| 159 | + |
| 160 | +**Rule of thumb**: If the user asks for "secure" or "signed" uploads, or needs to restrict uploads, use **signed** with a backend. For simple demos or when preset exposure is acceptable, **unsigned** is fine. |
| 161 | + |
| 162 | +## Secure (Signed) Uploads |
| 163 | + |
| 164 | +**Golden rules**: (1) **Never expose or commit the API secret** — it must live only in server env and server code. (2) **Never commit the API key or secret** — use `server/.env` (or equivalent) and ensure it is in `.gitignore`. (3) The **api_key** is not secret and may be sent to the client (e.g. in the signature response); only **api_secret** must stay server-only. |
| 165 | + |
| 166 | +**When the user asks for secure uploads**: Use a signed upload preset and generate the signature on the server. The client may receive `uploadSignature`, `uploadSignatureTimestamp`, `api_key`, and `cloudName` from your backend; it must **never** receive or contain the API secret. |
| 167 | + |
| 168 | +### Where to put API key and secret (server-only, never committed) |
| 169 | + |
| 170 | +- **Do not put them in the root `.env`** used by Vite. Keep root `.env` for `VITE_CLOUDINARY_CLOUD_NAME` and `VITE_CLOUDINARY_UPLOAD_PRESET` only. |
| 171 | +- **Create `server/.env`** (in a `server/` folder) and put there: `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET`. No `VITE_` prefix. Load this file only in the server process (e.g. `dotenv.config({ path: 'server/.env' })`). |
| 172 | +- **Never commit API key or secret**: Add `server/.env` to `.gitignore`. Use env vars for all credentials; never hardcode or commit them. |
| 173 | +- **In code**: Read `process.env.CLOUDINARY_API_SECRET` and `process.env.CLOUDINARY_API_KEY` only in server/API code. Never in React components or any file Vite bundles. |
| 174 | +- **Next.js**: `CLOUDINARY_*` in root `.env.local` is server-only (browser only sees `NEXT_PUBLIC_*`). For Vite + Node in same repo, prefer `server/.env` and load it only in the server. |
| 175 | +- **Server SDK**: Use the **Cloudinary Node.js SDK v2** for server-side signing: `import { v2 as cloudinary } from 'cloudinary'` (package name: `cloudinary`). Do **not** use v1 (e.g. 1.47.0) — v1 does not expose `cloudinary.utils.api_sign_request` the same way. Install: `npm install cloudinary` (v2). |
| 176 | + |
| 177 | +### How the client gets credentials (working pattern — use this) |
| 178 | + |
| 179 | +Use **`uploadSignature` as a function** (not `signatureEndpoint`). The widget calls the function with `params_to_sign`; your function calls your backend and passes the signature back. This pattern is reliable across widget versions. |
| 180 | + |
| 181 | +1. **Fetch `api_key` from server first** (before creating the widget). API key is not secret; safe to use in client. Your backend returns it from the sign endpoint (e.g. `/api/sign-image`). |
| 182 | + |
| 183 | +2. **Set `uploadSignature` to a function** that receives `(callback, params_to_sign)` from the widget. Inside the function, add `upload_preset` to `params_to_sign` (use your signed preset name, e.g. from env or a constant), POST to your backend with `{ params_to_sign: paramsWithPreset }`, then call `callback(data.signature)` with the response. |
| 184 | + |
| 185 | +3. **Include `uploadPreset` in the widget config** (your signed preset name). The widget needs it so it includes it in `params_to_sign`. **Default:** Cloudinary accounts have a built-in signed preset `ml_default` (users can delete it). If the user has not created their own signed preset, use `ml_default`; otherwise use the preset name from their dashboard. |
| 186 | + |
| 187 | +4. **Server endpoint**: Accept `params_to_sign` from the request body. Always include `upload_preset` in the object you sign (add it if the client did not send it). Use `cloudinary.utils.api_sign_request(paramsToSign, process.env.CLOUDINARY_API_SECRET)` to generate the signature. Return `{ signature, timestamp, api_key, cloud_name }`. Never return the API secret. |
| 188 | + |
| 189 | +**Preset name:** Use `ml_default` when the user has not specified a signed preset (Cloudinary provides it by default; users can delete it — then they must create one in the dashboard). Otherwise use the user's preset name. |
| 190 | + |
| 191 | +**Generic client pattern** (preset: use `ml_default` if it exists / user hasn't specified one; endpoint is up to the user): |
| 192 | +```tsx |
| 193 | +// Fetch api_key from server first, then: |
| 194 | +widgetConfig.api_key = data.api_key; // from your sign endpoint |
| 195 | +widgetConfig.uploadPreset = 'ml_default'; // default signed preset (or user's preset if they created one) |
| 196 | +widgetConfig.uploadSignature = function(callback, params_to_sign) { |
| 197 | + const paramsWithPreset = { ...params_to_sign, upload_preset: 'ml_default' }; |
| 198 | + fetch('/api/sign-image', { |
| 199 | + method: 'POST', |
| 200 | + headers: { 'Content-Type': 'application/json' }, |
| 201 | + body: JSON.stringify({ params_to_sign: paramsWithPreset }), |
| 202 | + }) |
| 203 | + .then(r => r.json()) |
| 204 | + .then(data => data.signature ? callback(data.signature) : callback('')) |
| 205 | + .catch(() => callback('')); |
| 206 | +}; |
| 207 | +``` |
| 208 | + |
| 209 | +**Generic server pattern** (Node/Express with SDK v2): |
| 210 | +```ts |
| 211 | +// import { v2 as cloudinary } from 'cloudinary'; |
| 212 | +const params = req.body.params_to_sign || {}; |
| 213 | +const paramsToSign = { ...params, upload_preset: params.upload_preset || 'ml_default' }; |
| 214 | +const signature = cloudinary.utils.api_sign_request(paramsToSign, process.env.CLOUDINARY_API_SECRET); |
| 215 | +res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CLOUDINARY_API_KEY, cloud_name: process.env.CLOUDINARY_CLOUD_NAME }); |
| 216 | +``` |
| 217 | + |
| 218 | +- ❌ **Avoid `signatureEndpoint`** — it may not be called reliably by all widget versions. Prefer the `uploadSignature` function. |
| 219 | +- ✅ Docs: [Upload widget — signed uploads](https://cloudinary.com/documentation/upload_widget#signed_uploads), [Upload assets in Next.js](https://cloudinary.com/documentation/upload_assets_in_nextjs_tutorial). |
| 220 | + |
| 221 | +### Rules for secure uploads |
| 222 | +- ✅ Use a **signed** upload preset (dashboard → Upload presets → Signed). Do not use an unsigned preset when the user wants secure uploads. **Default:** Accounts have a built-in signed preset `ml_default` — use it if the user hasn't created their own (they can delete `ml_default`, in which case they must create a signed preset in the dashboard). |
| 223 | +- ✅ Generate the signature **on the server only** using Cloudinary Node.js SDK **v2** (`cloudinary.utils.api_sign_request`). Never put `CLOUDINARY_API_SECRET` in a `VITE_` variable or in client-side code. |
| 224 | +- ✅ Keep `server/.env` in `.gitignore`; never commit API key or secret. |
| 225 | +- ✅ Use **`uploadSignature` as a function** (not `signatureEndpoint`) for reliable signed uploads. |
| 226 | +- ✅ Include **`uploadPreset` in the widget config** so the widget includes it in `params_to_sign`. |
| 227 | +- ✅ **Server must include `upload_preset` in the signed params** (add it if the client did not send it). |
| 228 | + |
| 229 | +### What not to do |
| 230 | +- ❌ **Never** put the API secret in a `VITE_` (or `NEXT_PUBLIC_`) variable or in any file sent to the browser. |
| 231 | +- ❌ **Never** commit the API key or secret; use env vars and ignore `server/.env` in git. |
| 232 | +- ❌ **Do not** generate the signature in client-side JavaScript (it would require the secret in the client). |
| 233 | +- ❌ **Do not** use an unsigned preset when the user explicitly wants secure/signed uploads. |
| 234 | +- ❌ **Do not** omit `uploadPreset` from the widget config when using signed uploads (widget needs it in `params_to_sign`). |
| 235 | +- ❌ **Do not** use Cloudinary Node SDK v1 (e.g. 1.47.0) for signing — use v2 (`import { v2 as cloudinary } from 'cloudinary'`). |
| 236 | +- ❌ **Do not** rely on `signatureEndpoint` alone; use the `uploadSignature` function for reliability. |
| 237 | + |
135 | 238 | ## Video Patterns |
136 | 239 |
|
137 | 240 | ### ⚠️ IMPORTANT: Two Different Approaches |
|
388 | 491 |
|
389 | 492 | ## Upload Widget Errors |
390 | 493 |
|
| 494 | +### Upload fails (unsigned uploads) — first check upload preset |
| 495 | +- ❌ Problem: Upload fails when using unsigned upload |
| 496 | +- ✅ **Debug checklist** (in order): |
| 497 | + 1. **Is the upload preset configured?** Check `.env` has `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name` (exact name, no typos) |
| 498 | + 2. **Does the preset exist?** Cloudinary dashboard → Settings → Upload → Upload presets |
| 499 | + 3. **Is it Unsigned?** Preset must be "Unsigned" for client-side uploads (no API key/secret in browser) |
| 500 | + 4. **Env reloaded?** Restart the dev server after any `.env` change |
| 501 | +- ✅ If all above are correct, then check: script loaded in `index.html`, cloud name set, and network/console for the actual error message |
| 502 | + |
391 | 503 | ### "Upload preset not found" or "Invalid upload preset" |
392 | 504 | - ❌ Problem: Preset doesn't exist or is signed |
393 | 505 | - ✅ Solution: |
|
412 | 524 | 2. Check widget initializes in `useEffect` after `window.cloudinary` is available |
413 | 525 | 3. Verify upload preset is set correctly |
414 | 526 |
|
| 527 | +### User needs secure/signed uploads |
| 528 | +- ❌ Problem: User asks for secure uploads; unsigned preset or client-side secret is not acceptable. |
| 529 | +- ✅ Use signed preset + server-side signature. Use **`uploadSignature` as a function** (not `signatureEndpoint`); fetch `api_key` from server first; include `uploadPreset` in widget config; server must include `upload_preset` in signed params. Use Cloudinary Node SDK **v2** on server. Never expose or commit the API secret. |
| 530 | +- ✅ See PATTERNS → "Signed vs unsigned uploads" and "Secure (Signed) Uploads" → "How the client gets credentials (working pattern)". |
| 531 | + |
| 532 | +### "Where do I put my API key and secret?" / "Never commit API key or secret" |
| 533 | +- ❌ Problem: User needs to store `CLOUDINARY_API_KEY` and `CLOUDINARY_API_SECRET` securely, or is told to "create a .env file" and worries it will overwrite the existing Vite `.env`. |
| 534 | +- ✅ Do not put them in root `.env`. Create `server/.env` with `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET`; add `server/.env` to `.gitignore`; load only in the server. Never commit API key or secret. |
| 535 | +- ✅ See PATTERNS → "Secure (Signed) Uploads" → "Where to put API key and secret (server-only, never committed)". |
| 536 | + |
| 537 | +### "Invalid Signature" or "Missing required parameter - api_key" |
| 538 | +- ❌ Problem: Signed upload fails with "Invalid Signature" or "Missing required parameter - api_key". |
| 539 | +- ✅ **Use the working pattern:** (1) Use **`uploadSignature` as a function** (not `signatureEndpoint`). (2) **Fetch `api_key` from server** before creating the widget (API key is not secret). (3) **Include `uploadPreset` in widget config** so the widget includes it in `params_to_sign`. (4) **Server must include `upload_preset` in the signed params** (add it if the client did not send it). (5) Use **Cloudinary Node.js SDK v2** on the server (`import { v2 as cloudinary } from 'cloudinary'`), not v1 (e.g. 1.47.0). |
| 540 | +- ✅ **Common mistakes:** Using `signatureEndpoint` instead of `uploadSignature` function; omitting `uploadPreset` from widget config; server not adding `upload_preset` to signature params; using SDK v1 for signing; not fetching `api_key` from server before creating the widget. If using `ml_default`, ensure it still exists (user may have deleted it); otherwise create a signed preset in the dashboard. |
| 541 | +- ✅ See PATTERNS → "Secure (Signed) Uploads" → "How the client gets credentials (working pattern)". |
| 542 | + |
415 | 543 | ## Video Errors |
416 | 544 |
|
417 | 545 | ### "AdvancedVideo not working" or "Video not displaying" |
|
509 | 637 | ## Quick Reference Checklist |
510 | 638 |
|
511 | 639 | When something isn't working, check: |
512 | | -- [ ] Environment variables have `VITE_` prefix |
| 640 | +- [ ] Environment variables have `VITE_` prefix (client); **never** put API secret in `VITE_` vars |
513 | 641 | - [ ] Dev server was restarted after .env changes |
514 | 642 | - [ ] Imports are from correct packages |
515 | 643 | - [ ] Transformations are chained on image instance |
516 | 644 | - [ ] Format/quality use separate `.delivery()` calls |
517 | 645 | - [ ] Plugins are in array format |
518 | 646 | - [ ] Upload widget script is loaded in `index.html` |
519 | | -- [ ] Upload preset is unsigned |
| 647 | +- [ ] **Upload fails (unsigned)?** → Is `VITE_CLOUDINARY_UPLOAD_PRESET` set? Preset exists and is Unsigned in dashboard? |
| 648 | +- [ ] **Secure uploads?** → Use `uploadSignature` as function (not `signatureEndpoint`); fetch `api_key` from server first; include `uploadPreset` in widget config; server includes `upload_preset` in signed params; use Cloudinary Node SDK v2 on server; never expose or commit API secret |
| 649 | +- [ ] **Where do API key/secret go?** → **Do not** put in root `.env`. Use **`server/.env`**; add to `.gitignore`; load only in server. **Never commit** API key or secret |
| 650 | +- [ ] Upload preset is unsigned (for simple client uploads) |
520 | 651 | - [ ] Video player is disposed in cleanup |
521 | 652 | - [ ] CSS files are imported for video player |
522 | 653 | - [ ] TypeScript types are properly imported |
|
0 commit comments