AuraImage

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:

.env.local
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.ai

Then 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.

OptionDefaultMinMax
expiresIn (seconds)360060604800 (7 days)
cdnUrlconstructor 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

ScenarioStatusBody
Public image, any URL200Image
Private image, no ?token=403{ "message": "Signed token required for private image" }
Private image, invalid / expired token403{ "message": "Invalid or expired signature" }
Private image, token for a different image403{ "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. serveSecret must 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 FlowsignUpload, edge validation, response shape.
  • Signature Spec — implementing serve-token signing in non-JS backends.