Skip to content

Commit 734d2e0

Browse files
author
strausr
committed
docs: update cursorrules for secure signed uploads
- Add Signed vs unsigned uploads education section - Document working pattern: uploadSignature as function (not signatureEndpoint) - Require Cloudinary Node SDK v2 for server signing (not v1) - Default to ml_default signed preset when user has no custom preset - Add COMMON ERRORS for Invalid Signature / Missing api_key - Golden rules: never expose or commit API secret; server-only env
1 parent d907c93 commit 734d2e0

1 file changed

Lines changed: 135 additions & 4 deletions

File tree

templates/.cursorrules.template

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
# Cloudinary React SDK Patterns & Common Errors
22

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+
35
## Official Documentation
46
- **Transformation Rules**: https://cloudinary.com/documentation/cloudinary_transformation_rules.md
57
- **Transformation Reference**: https://cloudinary.com/documentation/transformation_reference
68
- **React Image Transformations & Plugins**: https://cloudinary.com/documentation/react_image_transformations#plugins
79
- **React Video Transformations**: https://cloudinary.com/documentation/react_video_transformations
810
- **Cloudinary Video Player** (standalone player): https://cloudinary.com/documentation/cloudinary_video_player
911
- **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
1016
- Always consult the official transformation rules when creating transformations
1117
- Use only officially supported parameters from the transformation reference
1218

@@ -21,12 +27,17 @@
2127
- Always restart dev server after adding/updating `.env` variables
2228

2329
## 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
2632
- ✅ Set preset in `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name`
2733
- ✅ Use in code: `import { uploadPreset } from './cloudinary/config'`
2834
- ⚠️ If upload preset is missing, the Upload Widget will show an error message
2935
- ⚠️ 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`?
3041

3142
## Import Patterns
3243
- ✅ Import Cloudinary instance: `import { cld } from './cloudinary/config'`
@@ -132,6 +143,98 @@
132143
```
133144
- ✅ Upload result contains: `public_id`, `secure_url`, `width`, `height`, etc.
134145

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+
135238
## Video Patterns
136239

137240
### ⚠️ IMPORTANT: Two Different Approaches
@@ -388,6 +491,15 @@
388491

389492
## Upload Widget Errors
390493

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+
391503
### "Upload preset not found" or "Invalid upload preset"
392504
- ❌ Problem: Preset doesn't exist or is signed
393505
- ✅ Solution:
@@ -412,6 +524,22 @@
412524
2. Check widget initializes in `useEffect` after `window.cloudinary` is available
413525
3. Verify upload preset is set correctly
414526

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+
415543
## Video Errors
416544

417545
### "AdvancedVideo not working" or "Video not displaying"
@@ -509,14 +637,17 @@
509637
## Quick Reference Checklist
510638

511639
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
513641
- [ ] Dev server was restarted after .env changes
514642
- [ ] Imports are from correct packages
515643
- [ ] Transformations are chained on image instance
516644
- [ ] Format/quality use separate `.delivery()` calls
517645
- [ ] Plugins are in array format
518646
- [ ] 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)
520651
- [ ] Video player is disposed in cleanup
521652
- [ ] CSS files are imported for video player
522653
- [ ] TypeScript types are properly imported

0 commit comments

Comments
 (0)