Upload Flow
Edge Proxy Pattern — your Secret Key never leaves your server.
AuraImage uses the Edge Proxy Pattern. Your Secret Key never leaves your server; all upload requests are validated at the edge before a single byte reaches storage.
Architecture overview
[Your Backend] → Generate HMAC signature (signUpload)
↓
[Your Frontend] → POST file + signature → [AuraImage Edge]
↓
Pre-flight validation
(HMAC, expiry, magic bytes,
size, origin, rate limit, quota)
↓
[Object Storage] ← stored
↓
Returns final URL → Your appStep 1 — Generate a signature (server-side)
Your backend calls signUpload() using your Secret Key. This produces a short-lived HMAC token.
import { AuraImage } from '@auraimage/sdk';
const aura = new AuraImage({
secretKey: process.env.AURA_SECRET_KEY!,
projectName: process.env.NEXT_PUBLIC_AURA_PROJECT_NAME!
});
export async function POST() {
const signature = await aura.signUpload({
maxSize: '30mb', // enforced at the edge
allowedTypes: ['image/*'], // magic-byte validated
expiresIn: 3600 // seconds — token lifetime
});
return Response.json({ signature });
}import { Hono } from 'hono';
import { AuraImage } from '@auraimage/sdk';
const aura = new AuraImage({
secretKey: process.env.AURA_SECRET_KEY!,
projectName: process.env.NEXT_PUBLIC_AURA_PROJECT_NAME!
});
const app = new Hono();
app.post('/api/aura/sign', async (c) => {
const signature = await aura.signUpload({
maxSize: '30mb',
allowedTypes: ['image/*'],
expiresIn: 3600
});
return c.json({ signature });
});The signature encodes your constraints. An attacker who intercepts it cannot upload a larger file or a non-image format — the edge enforces these server-side.
For non-JS backends (Python, Ruby, Go, PHP, …), implement the HMAC scheme directly. See Signature Spec.
signUpload options
| Option | Type | Default | Description |
|---|---|---|---|
maxSize | string | number | "30mb" | Max file size. Bytes (number) or human form ("500kb", "30mb", "2gb"). |
allowedTypes | string[] | ["image/*"] | MIME patterns enforced via magic-byte detection. |
expiresIn | number | 3600 | Token lifetime in seconds. |
projectName | string | constructor value | Override the project for this token. |
visibility | "public" | "private" | "public" | Initial visibility — see Private Images. |
signUpload returns Promise<string>. Always await the call.
Step 2 — Upload from the client
// 1. Fetch a signature from your own backend
const { signature } = await fetch('/api/aura/sign', { method: 'POST' }).then((r) => r.json());
// 2. Build the form data
const formData = new FormData();
formData.append('file', file); // the image File object
formData.append('filename', 'hero-photo.jpg'); // optional; improves SEO
// 3. Upload to the AuraImage edge
const response = await fetch('https://cdn.auraimage.ai/v1/upload', {
method: 'POST',
headers: { 'X-Aura-Signature': signature },
body: formData
});
const { url, key, blurhash, width, height } = await response.json();Upload response
{
"url": "https://cdn.auraimage.ai/my-app/abc123xyz0-hero-photo.jpg",
"key": "abc123xyz0-hero-photo.jpg",
"blurhash": "LHF$Vb00~q~q9aM{RjxuIURjWBof",
"width": 2400,
"height": 1600,
"format": "jpeg",
"masterFormat": "jpeg",
"size": 184320,
"visibility": "public"
}| Field | Description |
|---|---|
url | Public CDN URL for the uploaded image. |
key | Stored filename — pass this to getSignedUrl() / setVisibility() (see Private Images). |
blurhash | ~30-char BlurHash string. Persist alongside the URL for instant placeholder rendering. |
width / height | Pixel dimensions of the original image. |
format | The format the user uploaded ("jpeg", "png", "heic", …). |
masterFormat | The format actually stored on the CDN. Same as format for browser-supported types; transcoded for HEIC / TIFF / BMP. |
size | Original byte size. |
visibility | "public" or "private" based on the signature's visibility option. |
url is final — store it. The randomized key prefix (10 chars) ensures same-name uploads don't collide.
Edge validation
The AuraImage edge runs these checks before the upload reaches storage:
| Check | Behavior on failure |
|---|---|
| HMAC signature | 401 — Verifies the request was authorized by your Secret Key. |
| Token expiry | 401 — Rejects tokens past their expiresIn window. |
| Reserved project names | 400 — api, admin, cdn, health, registry, static, test, v1 are reserved. |
| Allowed origins | 403 — Blocks uploads from origins not in your dashboard's allow-list (when configured). |
| Rate limiting | 429 — Per-project request throttling. |
| Quota | 402 — Plan storage / wallet-guard exceeded. |
Content-Type | 400 — Must be multipart/form-data. |
maxSize | 413 — Enforces the size constraint from the signature. |
| Magic bytes | 415 — Reads the first 12 bytes to confirm it's actually an image. |
allowedTypes | 415 — Detected format must match the signature's allow-list. |
| Tier-gated formats | 403 — HEIC / HEIF require Pro; TIFF / BMP require Startup. |
Input format handling
AuraImage accepts all common input formats:
- JPEG, PNG, WebP, AVIF, GIF — stored as-is.
- HEIC / HEIF (iPhone default) — Pro tier and above. Transcoded to a CDN-compatible master on ingest.
- TIFF / BMP — Startup tier. Transcoded to PNG or JPEG on ingest.
- JXL — not supported; returns
415.
The format field in the upload response reflects what you uploaded; masterFormat reflects what's stored. On delivery, the master is transcoded to AVIF, WebP, or JPEG via Accept-header negotiation. See URL API for content negotiation details.
BlurHash generation
AuraImage automatically computes a BlurHash for every uploaded image and returns it in the upload response. The <AuraImage /> component uses it for instant placeholder display before the full image arrives.
You can also fetch it later:
GET https://cdn.auraimage.ai/v1/blurhash/<projectName>/<filename>
→ { "blurhash": "LHF$Vb00~q~q9aM{RjxuIURjWBof" }Programmatic upload (CLI / MCP)
The aura CLI uploads entire directories:
# Upload all images in /public/assets and print the new URLs
aura upload ./public/assets --project-name my-app
# Emit newline-delimited JSON (filename, url, key, blurhash, dimensions, …) for scripting
aura upload ./public/assets --project-name my-app --json | jq '.url'The MCP migrate_assets tool does this automatically and rewrites your JSX in the same step. See AI Integration.