# Custom Storage (Bring Your Own Storage)

Store your renders in your own bucket. When custom storage is connected, every completed render is delivered to your AWS S3, Cloudflare R2, or any S3-compatible bucket, and the API response and webhooks include the URL of your copy in a `storage_url` field.

**Availability:** Scale and Enterprise plans. Configured by a team owner or admin in [Account Settings](https://app.templated.io/account).

## Storage modes

| Mode | What happens |
|------|--------------|
| **Keep a copy on Templated** (default) | Your bucket receives a copy of every render; Templated keeps its own copy too. Dashboard previews, merges and zips keep working exactly as before. |
| **Store only in my bucket** | Your bucket is the only durable home of the render. Templated's transient copy is deleted right after your copy is confirmed. The `url` field of the render points to your bucket. |

:::caution
With **Store only in my bucket**, dashboard previews and PDF merge/zip read the render from your bucket's URL. Make it publicly readable (set a **Public Base URL** backed by a CDN or a public bucket policy), or previews will not load. If a delivery to your bucket ever fails, Templated keeps its copy for that render, so a render never breaks. This also applies to the render download endpoint (`GET /v1/render/{id}`), which streams the file server-side, and to ZIP bundles, which skip files they cannot download. If a delivery to your bucket fails, Templated keeps its copy for that render, so only successfully delivered renders are affected.
:::

## Setup

1. In [Account Settings](https://app.templated.io/account), open **Custom Storage** → **Connect**.
2. Pick your provider: **AWS S3**, **Cloudflare R2**, or any S3-compatible service (MinIO, DigitalOcean Spaces...).
3. Fill in the credentials (see provider examples below), bucket name, and an optional folder path (defaults to `renders`).
4. Optionally set a **Public Base URL** (the public host where your objects are reachable, CDN or public bucket). It is used to build the `storage_url` returned by the API.
5. Choose the storage mode and hit **Connect**. The connection is tested automatically; you can re-run it anytime with **Test Connection**.

Objects are written as `{folder}/{render_id}.{format}` (or `{folder}/{render_id}/{name}.{format}` for named renders). Templated never reads or lists objects in your bucket, and never deletes your renders. The only delete Templated ever performs is removing its own connection-test probe object.

## Minimal IAM policy (AWS S3)

Templated only needs to write objects, plus delete for the connection-test probe object:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::YOUR_BUCKET/renders/*"
    }
  ]
}
```

No `GetObject`, no `ListBucket`, no ACL permissions: objects are written **without** an ACL header, so buckets with the modern Object Ownership default work as-is.

## Cloudflare R2

1. Create an R2 API token with **Object Read & Write** scoped to your bucket.
2. Use the S3 endpoint `https://<account-id>.r2.cloudflarestorage.com`, region `auto`.
3. R2 buckets have no public URL by default. Connect a [custom domain](https://developers.cloudflare.com/r2/buckets/public-buckets/) to the bucket and set it as the **Public Base URL**.

## API response

When custom storage is connected, render responses and webhooks include your copy:

```json
{
  "id": "9f24ff01-a03e-42f5-88ba-7dbec2771231",
  "status": "COMPLETED",
  "url": "https://cdn.templated.media/render/9f24ff01.png",
  "storage_url": "https://assets.yourcompany.com/renders/9f24ff01.png"
}
```

- `storage_url`: the confirmed URL of the copy in your bucket. `null` when custom storage is not connected or the delivery failed (Templated's copy always survives a failed delivery).
- With **Store only in my bucket**, `url` also points to your bucket after delivery.