Private Images
Gate images behind signed, expiring URLs.
Private images are stored unchanged on the CDN, but every read requires a short-lived serve token signed with your project's serve secret. Use them for paid content, internal dashboards, per-user uploads, or anything that shouldn't be world-readable.
Public images are the default — pass visibility: 'private' to signUpload() to opt in.
Setup
Each project has its own serve secret (psk_live_*), separate from your account-wide signing key (sk_live_*). Find it in the dashboard → your project → Settings → Serve Secret.
Add it to your environment alongside AURA_SECRET_KEY:
AURA_SECRET_KEY=sk_live_...
AURA_SERVE_SECRET=psk_live_...
NEXT_PUBLIC_AURA_PROJECT_NAME=my-app
NEXT_PUBLIC_AURA_CDN_URL=https://cdn.auraimage.aiThen construct the SDK with both secrets:
import { AuraImage } from '@auraimage/sdk';
const aura = new AuraImage({
secretKey: process.env.AURA_SECRET_KEY!,
serveSecret: process.env.AURA_SERVE_SECRET!,
projectName: process.env.NEXT_PUBLIC_AURA_PROJECT_NAME!,
cdnUrl: process.env.NEXT_PUBLIC_AURA_CDN_URL!
});serveSecret and cdnUrl are only required when you call getSignedUrl() or setVisibility(). Pure upload flows can omit them.
Upload as private
Pass visibility: 'private' on the upload signature:
const signature = await aura.signUpload({
maxSize: '5mb',
allowedTypes: ['image/*'],
expiresIn: 3600,
visibility: 'private'
});The visibility is encoded into the upload token; the edge stores the image as private and returns visibility: "private" in the upload response. Public reads of that image return 403.
Generate a signed read URL
Mint a per-request URL with a TTL on the server:
const url = await aura.getSignedUrl(uploadResponse.key, {
expiresIn: 600 // 10 minutes
});
// → https://cdn.auraimage.ai/my-app/abc123xyz0-photo.jpg?token=...Pass url straight into an <img>, <video poster>, <AuraImage> src, or <source srcset>. Once the token expires the URL stops resolving.
| Option | Default | Min | Max |
|---|---|---|---|
expiresIn (seconds) | 3600 | 60 | 604800 (7 days) |
cdnUrl | constructor value | — | — |
getSignedUrl() is server-only (it reads serveSecret). Never call it from browser code; render signed URLs in your server component / route handler / RSC.
Flip an existing image
setVisibility(filename, visibility) is idempotent and works without re-uploading:
await aura.setVisibility(uploadResponse.key, 'private');
// later:
await aura.setVisibility(uploadResponse.key, 'public');Returns { visibility: 'public' | 'private' }.
CDN behavior
| Scenario | Status | Body |
|---|---|---|
| Public image, any URL | 200 | Image |
Private image, no ?token= | 403 | { "message": "Signed token required for private image" } |
| Private image, invalid / expired token | 403 | { "message": "Invalid or expired signature" } |
| Private image, token for a different image | 403 | { "message": "Invalid or expired signature" } |
Caching
Public images: Cache-Control: public, max-age=31536000, immutable. Cached at the edge for instant repeat reads.
Private images: Cache-Control: private, max-age=300. Bypass the shared edge cache entirely — every request hits R2. This means:
- A signed URL is safe to share within its TTL — no other user will see a poisoned cache hit.
- High-volume private serving costs more than public (no shared cache amortization). Prefer public images when access doesn't actually need to be gated.
- Server-Timing on private responses always reflects an origin fetch.
Common pitfalls
- Don't sign in the browser.
serveSecretmust stay on the server. Mint URLs in a route handler / server action / RSC and render the result. - Don't reuse signed URLs across users. Two users with two TTLs is two
getSignedUrl()calls. The cost is negligible — it's a CPU operation, no network. - Don't combine
?token=with transform params via cache-busting. The token covers(project, filename, exp), not query params. Adding?w=400&token=...works, but the cache for transform variants is private + per-token-window. - Picking a TTL. Short TTLs (60–600s) for content that scrolls past quickly; longer (hours) for pages that linger. Max is 7 days.
See also
- Upload Flow —
signUpload, edge validation, response shape. - Signature Spec — implementing serve-token signing in non-JS backends.