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. |
name | string | random | Custom image name/path (e.g. "blog/hero"). Slashes create path segments. |
overwrite | boolean | false | Allow overwriting an existing image with the same name. Serve URLs are cache-immutable: already-cached variants may keep serving for up to 30 days — bump a ?v= query param on the serve URL (or use a new name) for instant freshness. |
signUpload returns Promise<string>. Always await the call.
Server-side upload (from URL)
Use uploadFromUrl() when you already have the image at a public URL — no browser or FormData needed. The SDK fetches the bytes, validates the response, and uploads directly to Auraimage in one call.
Requires cdnUrl set on the AuraImage constructor.
import { AuraImage } from '@auraimage/sdk';
const aura = new AuraImage({
secretKey: process.env.AURA_SECRET_KEY!,
projectName: process.env.AURA_PROJECT!,
cdnUrl: 'https://cdn.auraimage.ai'
});
const result = await aura.uploadFromUrl('https://example.com/photo.jpg');
// { url, name, blurhash, width, height, format, masterFormat, size, visibility }uploadFromUrl options
| Option | Type | Default | Description |
|---|---|---|---|
name | string | derived from URL | Custom image name/path (extension-less). If omitted, taken from the URL's last path segment with the extension stripped (e.g. .../cat.jpg → "cat"). |
maxSize | string | number | "30mb" | Max file size. Bytes or human form ("50mb"). Checked against Content-Length header first, then actual downloaded bytes. |
visibility | "public" | "private" | "public" | Initial visibility — see Private Images. |
overwrite | boolean | false | Allow overwriting an existing image with the same name. Serve URLs are cache-immutable: already-cached variants may keep serving for up to 30 days — bump a ?v= query param on the serve URL (or use a new name) for instant freshness. |
timeout | number | 30000 | Fetch timeout in milliseconds. |
Error handling
uploadFromUrl throws UploadFromUrlError with a kind discriminant:
| Kind | When |
|---|---|
invalid-name | No name was provided and none could be derived from the URL. |
fetch | The remote URL is unreachable, returns a non-2xx status, times out, or the response body can't be read. |
content-type | The remote server returned a non-image Content-Type. |
size | The downloaded file exceeds maxSize. |
upload | The CDN rejected the upload (409 conflict, 413 too large, 415 unsupported format, etc.). |
The url property on the error identifies the source URL that failed, and status carries the HTTP status code when available.
import { AuraImage, UploadFromUrlError } from '@auraimage/sdk';
try {
const result = await aura.uploadFromUrl('https://example.com/photo.jpg');
} catch (err) {
if (err instanceof UploadFromUrlError) {
console.error(`Failed (${err.kind}) for ${err.url}: ${err.message}`);
}
}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, name, blurhash, width, height } = await response.json();Upload response
{
"url": "https://cdn.auraimage.ai/my-app/abc123xyz0-hero-photo",
"name": "abc123xyz0-hero-photo",
"blurhash": "LHF$Vb00~q~q9aM{RjxuIURjWBof",
"width": 2400,
"height": 1600,
"format": "jpeg",
"masterFormat": "jpeg",
"size": 184320,
"visibility": "public"
}| Field | Description |
|---|---|
url | Canonical, extension-less CDN URL for the uploaded image. |
name | Stored image name (extension-less) — 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. Auto-generated names carry a randomized 10-char prefix, so uploads without an explicit name claim 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. |
Retries & backpressure
Under heavy load the edge can reject an upload with a transient status. Every
transient rejection carries a Retry-After header (in seconds) — that is the
contract to retry against:
| Status | Meaning | What to do |
|---|---|---|
429 | Per-project rate limit (1,200 requests/min). | Wait Retry-After, then retry. |
503 + Retry-After | Origin at capacity (queue full). | Wait Retry-After, then retry. |
503 + X-Aura-Origin-State: draining | The origin task is shutting down. | Retry immediately — a fresh request lands on a healthy task. |
A compliant client:
- Retries
429,503, and network failures up to 4 attempts total. - Waits
Retry-Afterseconds (falling back to 500 ms when absent) plus random jitter between attempts, so a batch doesn't retry in lockstep. - Keeps batch concurrency at ≤ 8 simultaneous uploads.
- For large batches, collects files that exhausted their attempts and re-tries them once more at low concurrency after the batch drains — by then the contention that failed them has usually passed.
uploadMany in @auraimage/sdk/client (and the <AuraUploader />
component) implements all of this out of the box — a 1,000-file batch
completes without any retry code on your side. If you upload via raw
fetch, implement the steps above; a client that ignores Retry-After
will lose files under load and that's expected behavior, not a platform
fault.
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>/<name>
→ { "blurhash": "LHF$Vb00~q~q9aM{RjxuIURjWBof" }This endpoint is cached like image serving — shared cache, 30-day TTL, ?v honored as a cache-buster, and 404s negatively cached for 60s. For a private image it requires the image's serve token (see Private Images).
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, name, 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.