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

# Storage Buckets

> Bring your own S3. Register multiple buckets per tenant and route each file context to the bucket that fits — with automatic fallback to a default.

## Overview

Uplint stores every accepted file in **your own** Amazon S3, using credentials you provide. A **storage bucket** (also called a storage configuration) bundles the S3 details Uplint needs to read and write objects on your behalf:

* The **bucket name** and **AWS region**
* An **access key ID** and **secret access key** (encrypted at rest, never returned)
* A **base path** prefix applied to every object written to the bucket
* A human-friendly **name** used to identify the bucket in the dashboard and API

A tenant can register **many** buckets. One of them is always the **default (fallback) bucket**, and each [file context](/concepts/file-contexts) can be routed to any active bucket you choose. This lets you keep, say, patient documents, avatars, and exports in separate buckets — each with its own credentials, region, and lifecycle — while still driving everything through one Uplint API.

<Note>
  All storage bucket endpoints require an API key with the `admin` scope. Managing storage is an administrative operation, distinct from the `upload`/`download` scopes used for day-to-day file traffic.
</Note>

## Why multiple buckets?

A single bucket works fine when everything you ingest belongs together. Multiple buckets become useful when different kinds of uploads have different requirements:

<CardGroup cols={2}>
  <Card title="Data separation" icon="folder-tree">
    Keep regulated data (e.g. medical claims) in a dedicated bucket, isolated from lower-sensitivity uploads like profile photos.
  </Card>

  <Card title="Data residency" icon="earth-americas">
    Route EU uploads to an `eu-west-1` bucket and APAC uploads to `ap-south-1` to satisfy residency requirements.
  </Card>

  <Card title="Independent credentials" icon="key">
    Give each bucket its own IAM key so you can rotate or revoke access for one workload without touching the others.
  </Card>

  <Card title="Cost & lifecycle" icon="scale-balanced">
    Apply different S3 lifecycle and storage-class policies per bucket, matched to how long each data type must live.
  </Card>
</CardGroup>

## The default bucket

Exactly one bucket is the tenant's **default** at any time. The default is the fallback Uplint uses whenever a more specific bucket can't be determined — for example, when a file context isn't bound to a bucket.

* The **first** bucket you add automatically becomes the default.
* Create a bucket with `set_as_default: true` to make it the default on creation.
* Promote an existing bucket at any time with the [Set Default Bucket](/api-reference/storage/set-default) endpoint. Promoting one bucket demotes the previous default.
* The default bucket can't be deleted. Set a different default first.

## Routing: which bucket a file goes to

Every file context has an optional `storage_config_id`. That single field decides where a context's uploads land.

### On upload

When a file is uploaded into a context, Uplint resolves the target bucket in this order:

1. The context's `storage_config_id`, if it points to an **active** bucket.
2. Otherwise — unset, missing, or inactive — the tenant's **default** bucket.

<Tip>
  Binding a context to a bucket is just setting `storage_config_id` on the context. Send an empty string when updating a context to unbind it and fall back to the default bucket.
</Tip>

### On download

Uplint records which bucket each file was written to, so files remain readable even after you add, rename, or re-route buckets. When serving a download it resolves the bucket in this order:

1. The file's stored `storage_config_id`.
2. Otherwise, a bucket whose name matches the one recorded on the file (this keeps files uploaded before multi-bucket routing readable).
3. Otherwise, the tenant's **default** bucket.

Because routing is resolved per file, re-pointing a context to a different bucket only affects **new** uploads. Existing files keep resolving to the bucket they were originally written to.

## Immutable fields

Once a bucket is created, its `default_bucket` (the S3 bucket name) and `base_path` **cannot be changed** — doing so would orphan every file already stored under them. To move data to a different bucket or prefix, add a new storage bucket and route your contexts to it.

You **can** always update the safe fields:

| Field                                 | Why you'd change it                                           |
| ------------------------------------- | ------------------------------------------------------------- |
| `name`                                | Rename the bucket's label.                                    |
| `access_key_id` / `secret_access_key` | Rotate AWS credentials.                                       |
| `region`                              | Reflect a bucket that has moved region.                       |
| `is_active`                           | Enable or disable the bucket for routing without deleting it. |

## Credential handling

Secret access keys are **encrypted at rest** and are **never** returned by any endpoint. When you read a bucket, the `access_key_id` comes back masked (e.g. `AKIA...MPLE`) so you can recognize which key is in use without exposing it. To change a key, submit a new `access_key_id` and `secret_access_key` via [Update Storage Bucket](/api-reference/storage/update).

## Deleting a bucket

Deletion is refused when the bucket is either:

* the tenant's **default** bucket, or
* still **bound to one or more file contexts**.

Reassign or unbind those contexts (and, if needed, promote another bucket to default) before deleting. This guard prevents accidentally cutting off the storage that live contexts depend on.

## Worked example

Register a dedicated bucket for insurance claims, then route a context to it.

<Steps>
  <Step title="Add the bucket">
    ```bash theme={null}
    curl -X POST https://api.uplint.dev/api/v1/tenants/tnt_9f8e7d6c/storage-configs \
      -H "Authorization: Bearer ul_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Insurance Claims",
        "access_key_id": "AKIAIOSFODNN7EXAMPLE",
        "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "region": "ap-south-1",
        "default_bucket": "acme-insurance-claims",
        "base_path": "file-service/"
      }'
    ```

    The response returns a `config_id` (e.g. `cfg_a1b2c3d4e5f6`). The access key comes back masked and the secret is omitted.
  </Step>

  <Step title="Route a context to it">
    ```bash theme={null}
    curl -X PATCH https://api.uplint.dev/api/v1/contexts/insurance_claims \
      -H "Authorization: Bearer ul_live_..." \
      -H "Content-Type: application/json" \
      -d '{ "storage_config_id": "cfg_a1b2c3d4e5f6" }'
    ```

    New uploads into `insurance_claims` now land in `acme-insurance-claims`. Every other context continues to use whatever bucket it was routed to, or the default.
  </Step>

  <Step title="(Optional) Make it the default">
    ```bash theme={null}
    curl -X POST \
      https://api.uplint.dev/api/v1/tenants/tnt_9f8e7d6c/storage-configs/cfg_a1b2c3d4e5f6/set-default \
      -H "Authorization: Bearer ul_live_..."
    ```
  </Step>
</Steps>

## Backward compatibility

Multi-bucket routing is fully backward compatible. Tenants that already had a single storage configuration keep working with zero changes: their existing config becomes the default bucket, and files uploaded before routing existed continue to resolve by their recorded bucket name. You only opt into multiple buckets when you add a second one.

## API reference

<CardGroup cols={2}>
  <Card title="List storage buckets" icon="list" href="/api-reference/storage/list">
    Every bucket configured for the tenant.
  </Card>

  <Card title="Add a storage bucket" icon="plus" href="/api-reference/storage/add">
    Register a new S3 bucket.
  </Card>

  <Card title="Get a storage bucket" icon="magnifying-glass" href="/api-reference/storage/get">
    Fetch a single bucket by ID.
  </Card>

  <Card title="Update a storage bucket" icon="pen" href="/api-reference/storage/update">
    Rotate keys, rename, or toggle active.
  </Card>

  <Card title="Set default bucket" icon="star" href="/api-reference/storage/set-default">
    Choose the tenant's fallback bucket.
  </Card>

  <Card title="Delete a storage bucket" icon="trash" href="/api-reference/storage/delete">
    Remove an unused bucket.
  </Card>
</CardGroup>
