> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ub.bitbros.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage

> Upload files and images to get public CDN URLs. No S3 buckets or complex configuration required.

urBackend handles file and image uploads for you. Upload a file and receive a public CDN URL you can use directly in your application.

**Base URL:** `https://api.ub.bitbros.in`

<Warning>
  All storage endpoints require your **secret key** (`sk_live_...`) in the `x-api-key` header. These operations should only be performed server-side.
</Warning>

<Warning>
  All uploaded files are **publicly accessible** via the returned URL. Do not upload sensitive or private documents.
</Warning>

## Upload a file

Uploads use a presigned URL three-step flow so the binary is sent directly to storage.

### Step 1 — Request an upload URL

**Endpoint:** `POST /api/storage/upload-request`

**Request body (size in bytes):**

```json theme={null}
{
  "filename": "avatar.png",
  "contentType": "image/png",
  "size": 123456
}
```

**Response:**

```json theme={null}
{
  "signedUrl": "https://storage.example.com/...",
  "token": "optional-provider-token",
  "filePath": "PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png"
}
```

| Field       | Description                                                                                                      |
| ----------- | ---------------------------------------------------------------------------------------------------------------- |
| `signedUrl` | Presigned URL to upload the binary directly to storage                                                           |
| `token`     | Provider-specific token (only returned for some external providers)                                              |
| `filePath`  | Opaque storage path (format: `projectId/uuid_sanitizedFilename`) — save this to confirm or delete the file later |

<Note>
  This endpoint enforces the 10 MB max file size and checks available headroom against your plan's project storage quota before the upload is confirmed. On the free tier, the default project storage limit is 20 MB.
</Note>

### Step 2 — Upload the binary to storage

Send a browser `PUT` request to `signedUrl` with the raw file contents and the correct `Content-Type` header.

```javascript theme={null}
const uploadRes = await fetch(signedUrl, {
  method: 'PUT',
  headers: {
    'Content-Type': file.type
  },
  body: file
});

if (!uploadRes.ok) {
  throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
}
```

### Step 3 — Confirm the upload

**Endpoint:** `POST /api/storage/upload-confirm`

**Request body:**

```json theme={null}
{
  "filePath": "PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png",
  "size": 123456
}
```

**Response:**

```json theme={null}
{
  "message": "Upload confirmed",
  "path": "PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png",
  "provider": "internal",
  "url": "https://xyz.supabase.co/storage/v1/object/public/dev-files/PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png"
}
```

| Field      | Description                                                 |
| ---------- | ----------------------------------------------------------- |
| `message`  | Confirmation message                                        |
| `path`     | Normalized storage path — use this to delete the file later |
| `provider` | Storage backend used (`internal` or `external`)             |
| `url`      | Public URL for the file. If unavailable, this is `null`.    |
| `warning`  | Optional warning message when a public URL is unavailable   |

If the storage provider does not expose a public URL (for example, some external S3/R2 setups), the response returns `url: null` and includes a `warning` field explaining that a public URL is not available.

<Note>
  The confirm step verifies the object exists, checks the size matches, and then charges quota atomically.
</Note>

### Full browser example

```javascript theme={null}
if (!fileInput?.files?.length) {
  throw new Error('No file selected');
}

const file = fileInput.files[0];

const requestRes = await fetch('https://api.ub.bitbros.in/api/storage/upload-request', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'sk_live_YOUR_KEY'
  },
  body: JSON.stringify({
    filename: file.name,
    contentType: file.type,
    size: file.size
  })
});

if (!requestRes.ok) {
  const errorBody = await requestRes.json().catch(() => null);
  throw new Error(
    `Upload request failed: ${requestRes.status} ${requestRes.statusText}${
      errorBody?.error ? ` - ${errorBody.error}` : errorBody?.message ? ` - ${errorBody.message}` : ''
    }`
  );
}

const { signedUrl, filePath } = await requestRes.json();

const uploadRes = await fetch(signedUrl, {
  method: 'PUT',
  headers: { 'Content-Type': file.type },
  body: file
});

if (!uploadRes.ok) {
  throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
}

const confirmRes = await fetch('https://api.ub.bitbros.in/api/storage/upload-confirm', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'sk_live_YOUR_KEY'
  },
  body: JSON.stringify({ filePath, size: file.size })
});

if (!confirmRes.ok) {
  if (confirmRes.status === 409) {
    const retryableError = await confirmRes.json();
    throw new Error(
      `Upload not ready yet: ${retryableError.error || retryableError.code || 'UPLOAD_NOT_READY'}. Retry the confirm request shortly.`
    );
  }

  const errorBody = await confirmRes.json().catch(() => null);
  throw new Error(
    `Upload confirm failed: ${confirmRes.status} ${confirmRes.statusText}${
      errorBody?.error ? ` - ${errorBody.error}` : errorBody?.message ? ` - ${errorBody.message}` : ''
    }`
  );
}

const { url, provider, path, warning } = await confirmRes.json();
console.log(url, provider, path, warning);
```

<Note>
  SDK users (`urbackend-sdk`) do not need to change anything — `client.storage.upload()` uses this flow internally.
</Note>

## Required bucket CORS (S3/R2)

If you use AWS S3 or Cloudflare R2 with presigned browser uploads, configure bucket CORS so browser uploads to `signedUrl` can pass preflight.

Required methods:

* `PUT`
* `OPTIONS`
* `GET`
* `HEAD`

Required `AllowedHeaders` (not `ExposeHeaders`) should include at least:

* `content-type` — required for presigned `PUT` requests
* `content-length` — typically safelisted, but some providers still require it to be allow-listed

Without these CORS rules, browser uploads can fail even when `POST /api/storage/upload-request` and `POST /api/storage/upload-confirm` are correct.

## Delete a file

To delete a file, pass the `path` returned from the upload response.

**Endpoint:** `DELETE /api/storage/file`

```javascript theme={null}
await fetch('https://api.ub.bitbros.in/api/storage/file', {
  method: 'DELETE',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'sk_live_YOUR_KEY'
  },
  body: JSON.stringify({
    path: 'YOUR_PROJECT_ID/your_file_name.png'
  })
});
```

If the path is invalid or the file has already been removed, the API returns `404`.

## Limits

| Limit                     | Value                                          |
| ------------------------- | ---------------------------------------------- |
| Maximum file size         | 10 MB per file                                 |
| Total storage per project | 20 MB (free tier; higher limits on paid plans) |

## Troubleshooting

<AccordionGroup>
  <Accordion title="400 Bad Request">
    Ensure `filename`, `contentType`, and `size` are provided for the upload request, and that `filePath` and `size` are sent to the confirm endpoint.
  </Accordion>

  <Accordion title="401 Unauthorized">
    The `x-api-key` header is missing or contains an invalid key. Check that you are using a valid key from your project dashboard.
  </Accordion>

  <Accordion title="413 Payload Too Large">
    The file exceeds the 10 MB per-file limit. Compress or resize the file before uploading.
  </Accordion>

  <Accordion title="CORS errors on PUT to signedUrl">
    If you use an external S3/R2 bucket, configure bucket CORS to allow `PUT`, `OPTIONS`, `GET`, and `HEAD`, and allow `content-type` and `content-length` headers from your client origin.
  </Accordion>
</AccordionGroup>
