# Upload Flow (https://auraimage.ai/docs/upload)



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 [#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 app
```

## Step 1 — Generate a signature (server-side) [#step-1--generate-a-signature-server-side]

Your backend calls `signUpload()` using your **Secret Key**. This produces a short-lived HMAC token.

<Tabs items="['Next.js (App Router)', 'Hono']">
  <Tab>
    ```ts title="app/api/aura/sign/route.ts"
    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 });
    }
    ```
  </Tab>

  <Tab>
    ```ts
    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 });
    });
    ```
  </Tab>
</Tabs>

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](/docs/signature-spec).

### `signUpload` options [#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](/docs/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) [#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.

```ts
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 [#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](/docs/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 [#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.

```ts
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 [#step-2--upload-from-the-client]

```ts
// 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 [#upload-response]

```json
{
  "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](/docs/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 [#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 [#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**:

1. Retries `429`, `503`, and network failures up to **4 attempts** total.
2. Waits `Retry-After` seconds (falling back to 500 ms when absent) **plus
   random jitter** between attempts, so a batch doesn't retry in lockstep.
3. Keeps batch concurrency at **≤ 8 simultaneous uploads**.
4. 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](/docs/uploader)) 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 [#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](/docs/url-api) for content negotiation details.

## BlurHash generation [#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](/docs/private-images)).

## Programmatic upload (CLI / MCP) [#programmatic-upload-cli--mcp]

The `aura` CLI uploads entire directories:

```bash
# 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](/docs/ai-integration).


## Related

- [Uploader](https://auraimage.ai/docs/uploader)
- [Private Images](https://auraimage.ai/docs/private-images)
- [Dashboard](https://auraimage.ai/docs/dashboard)