# Object storage (/platform/self-hosting/object-storage)





Configure file storage before uploading durable files or adding app replicas. Sim supports local disk, AWS S3, Azure Blob Storage, and Google Cloud Storage (GCS).

| Backend | When to use |
| - | - |
| **Local disk** | Single-node Docker, local development, evaluation |
| **[AWS S3](https://aws.amazon.com/s3/)** | Production, especially when running more than one app replica |
| **[Azure Blob](https://learn.microsoft.com/azure/storage/blobs/)** | Production on Azure |
| **[Google Cloud Storage](https://cloud.google.com/storage)** | Production on GCP |

<Callout type="warn">
  Local disk writes to `/app/apps/sim/uploads` inside the container. Neither `docker-compose.prod.yml` nor the Helm chart mounts a volume there, so every uploaded file is lost the moment the container is recreated — and files are **not** shared across replicas. Mount your own volume at that path if you must stay on local disk; for any multi-replica or production deployment, use S3, Azure Blob, or Google Cloud Storage.
</Callout>

## How the backend is selected [#how-the-backend-is-selected]

Set `STORAGE_PROVIDER` to `local`, `s3`, `azure`, or `gcs` to select a backend explicitly. When it is unset, Sim infers the backend from the configured environment variables in this order:

1. **Azure Blob Storage** — used if `AZURE_STORAGE_CONTAINER_NAME` is set **and** either (`AZURE_ACCOUNT_NAME` + `AZURE_ACCOUNT_KEY`) or `AZURE_CONNECTION_STRING` is set.
2. **AWS S3** — used if `S3_BUCKET_NAME` **and** `AWS_REGION` are set (and Azure is not configured).
3. **Google Cloud Storage** — used if `GCS_BUCKET_NAME` is set (and neither Azure nor S3 is configured).
4. **Local disk** — the fallback when no cloud backend is activated. Azure activates from its main container or credentials; S3 activates from any of its bucket names or `S3_ENDPOINT`; GCS activates from `GCS_BUCKET_NAME`. Dedicated Azure container names or GCS bucket names alone do not activate those providers.

If `STORAGE_PROVIDER` is unset, Sim uses the first backend whose configuration is complete, in that order. An explicit `STORAGE_PROVIDER` takes precedence and must be valid and complete.

A partially configured backend is skipped, so a fully configured one later in the order still wins. A backend whose values are present but malformed is different: it stops startup immediately, and a later fully configured one does not take over. An activated but incomplete backend does not fall back to local disk: if no other backend is complete, startup fails with `File storage is partially or incorrectly configured`. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are checked as a pair — set both or neither.

## Set up AWS S3 [#set-up-aws-s3]

<Steps>
  <Step>
    ### Create the buckets [#create-the-buckets]

    Sim separates files into purpose-specific buckets. **Sim never creates buckets** — create each one yourself before configuring it. Most S3 bucket variables do **not** fall back to `S3_BUCKET_NAME`: leave `S3_KB_BUCKET_NAME`, `S3_CHAT_BUCKET_NAME`, `S3_COPILOT_BUCKET_NAME`, or `S3_PROFILE_PICTURES_BUCKET_NAME` unset and it resolves to an empty bucket name, so that file type fails to store. Three behave differently — `S3_OG_IMAGES_BUCKET_NAME` and `S3_WORKSPACE_LOGOS_BUCKET_NAME` fall back to `S3_BUCKET_NAME`, and `S3_EXECUTION_FILES_BUCKET_NAME` falls back to a literal default name (see the reference table below).

    ```bash
    # Set your region once
    export AWS_REGION=us-east-1

    # The eight purpose-specific buckets. The CORS and IAM steps below reuse this list.
    export SIM_BUCKETS="workspace-files knowledge-base execution-files chat-files
                        copilot-files profile-pictures og-images workspace-logos"

    # Create buckets (names must be globally unique — prefix with your org)
    for name in ${SIM_BUCKETS:?run the export above first}; do
      if [ "$AWS_REGION" = "us-east-1" ]; then
        aws s3api create-bucket --bucket "myorg-sim-$name" --region "$AWS_REGION"
      else
        aws s3api create-bucket \
          --bucket "myorg-sim-$name" \
          --region "$AWS_REGION" \
          --create-bucket-configuration LocationConstraint="$AWS_REGION"
      fi
    done
    ```

    The command omits `LocationConstraint` in `us-east-1`, which requires that exception. See [AWS CreateBucket](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html).

    Keep all buckets **private** (block public access). Uploads use short-lived signed URLs and downloads are proxied through the app. Buckets do not need public read access.
  </Step>

  <Step>
    ### Configure CORS on every bucket [#configure-cors-on-every-bucket]

    Uploads are sent **directly from the browser** to S3 via presigned `PUT` requests, so each bucket needs a CORS policy that allows your Sim origin. A missing or mismatched policy can block browser uploads even when server credentials work.

    ```bash
    cat > /tmp/cors.json <<'EOF'
    {
      "CORSRules": [
        {
          "AllowedOrigins": ["https://sim.yourdomain.com"],
          "AllowedMethods": ["GET", "PUT"],
          "AllowedHeaders": ["*"],
          "MaxAgeSeconds": 3600
        }
      ]
    }
    EOF

    for name in ${SIM_BUCKETS:?run the export above first}; do
      aws s3api put-bucket-cors --bucket "myorg-sim-$name" --cors-configuration file:///tmp/cors.json
    done
    ```

    Set `AllowedOrigins` to your exact Sim origin (scheme + host, no trailing slash). Add every origin users reach Sim from, including an apex/`www` pair if both are live.
  </Step>

  <Step>
    ### Grant access with an IAM policy [#grant-access-with-an-iam-policy]

    Create an IAM policy scoped to your buckets and attach it to the user (or role) Sim runs as:

    ```json
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:GetObject",
            "s3:PutObject",
            "s3:DeleteObject",
            "s3:ListBucket",
            "s3:AbortMultipartUpload",
            "s3:ListMultipartUploadParts"
          ],
          "Resource": [
            "arn:aws:s3:::myorg-sim-*",
            "arn:aws:s3:::myorg-sim-*/*"
          ]
        }
      ]
    }
    ```

    You then have two ways to supply credentials:

    * **Static keys** — create an IAM user with this policy and set `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`.
    * **Instance/role credentials (recommended)** — attach the policy to the EC2 instance role, ECS task role, or EKS IRSA role. Leave `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` unset and Sim falls back to the default AWS credential chain automatically.
  </Step>

  <Step>
    ### Configure environment variables [#configure-environment-variables]

    Set the region, optionally the credentials, and the bucket names:

    ```bash
    # Region + credentials
    AWS_REGION=us-east-1
    AWS_ACCESS_KEY_ID=AKIA...          # omit when using an instance/IRSA role
    AWS_SECRET_ACCESS_KEY=...          # omit when using an instance/IRSA role

    # Buckets (per purpose)
    S3_BUCKET_NAME=myorg-sim-workspace-files
    S3_KB_BUCKET_NAME=myorg-sim-knowledge-base
    S3_EXECUTION_FILES_BUCKET_NAME=myorg-sim-execution-files
    S3_CHAT_BUCKET_NAME=myorg-sim-chat-files
    S3_COPILOT_BUCKET_NAME=myorg-sim-copilot-files
    S3_PROFILE_PICTURES_BUCKET_NAME=myorg-sim-profile-pictures
    S3_OG_IMAGES_BUCKET_NAME=myorg-sim-og-images
    S3_WORKSPACE_LOGOS_BUCKET_NAME=myorg-sim-workspace-logos
    ```

    `AWS_REGION` and `S3_BUCKET_NAME` are what switch Sim into S3 mode, but they do not cover the other file types — set every bucket above, because only the OpenGraph and workspace-logo buckets fall back to the general one.
  </Step>
</Steps>

### S3 bucket reference [#s3-bucket-reference]

| Variable | Stores | Required |
| - | - | - |
| `AWS_REGION` | Region for all buckets | **Yes** (enables S3) |
| `AWS_ACCESS_KEY_ID` | Access key | No (uses credential chain if unset) |
| `AWS_SECRET_ACCESS_KEY` | Secret key | No (uses credential chain if unset) |
| `S3_BUCKET_NAME` | General workspace files | **Yes** (enables S3) |
| `S3_KB_BUCKET_NAME` | Knowledge base documents | **Yes**, to use knowledge bases (no fallback) |
| `S3_EXECUTION_FILES_BUCKET_NAME` | Workflow execution files. Falls back to the literal name `sim-execution-files`, which may not exist or belong to you — set this explicitly | Set explicitly to your own bucket |
| `S3_CHAT_BUCKET_NAME` | Deployed chat assets | **Yes**, to use deployed chats (no fallback) |
| `S3_COPILOT_BUCKET_NAME` | Chat attachments | **Yes**, to attach files in Chat (no fallback) |
| `S3_PROFILE_PICTURES_BUCKET_NAME` | User avatars | **Yes**, for avatar uploads (no fallback) |
| `S3_OG_IMAGES_BUCKET_NAME` | OpenGraph preview images | Optional (falls back to `S3_BUCKET_NAME`) |
| `S3_WORKSPACE_LOGOS_BUCKET_NAME` | Workspace logos | Optional (falls back to `S3_BUCKET_NAME`) |
| `S3_ENDPOINT` | Custom endpoint for S3-compatible storage (R2, MinIO, B2) | Optional (AWS S3 if unset) |
| `S3_FORCE_PATH_STYLE` | `true` for path-style addressing (MinIO/Ceph) | Optional (defaults `false`) |

## Set up Azure Blob Storage [#set-up-azure-blob-storage]

Create one private container per purpose; Sim does not create them. Authenticate with either a connection string or an account name and key.

```bash
# Credentials — provide ONE of these forms
AZURE_ACCOUNT_NAME=mystorageaccount
AZURE_ACCOUNT_KEY=...
# or
AZURE_CONNECTION_STRING='DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net'

# Containers (per purpose)
AZURE_STORAGE_CONTAINER_NAME=workspace-files
AZURE_STORAGE_KB_CONTAINER_NAME=knowledge-base
AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME=execution-files
AZURE_STORAGE_CHAT_CONTAINER_NAME=chat-files
AZURE_STORAGE_COPILOT_CONTAINER_NAME=copilot-files
AZURE_STORAGE_PROFILE_PICTURES_CONTAINER_NAME=profile-pictures
AZURE_STORAGE_OG_IMAGES_CONTAINER_NAME=og-images
AZURE_STORAGE_WORKSPACE_LOGOS_CONTAINER_NAME=workspace-logos
```

Create all eight containers before applying the variables. For example, with account-name/key credentials:

```bash
for name in workspace-files knowledge-base execution-files chat-files copilot-files profile-pictures og-images workspace-logos; do
  az storage container create \
    --name "$name" \
    --account-name "$AZURE_ACCOUNT_NAME" \
    --account-key "$AZURE_ACCOUNT_KEY" \
    --public-access off
done
```

For connection-string credentials, replace the account options with `--connection-string "$AZURE_CONNECTION_STRING"`.

| Container variable | Fallback when unset |
| - | - |
| `AZURE_STORAGE_KB_CONTAINER_NAME`, `AZURE_STORAGE_CHAT_CONTAINER_NAME`, `AZURE_STORAGE_COPILOT_CONTAINER_NAME`, `AZURE_STORAGE_PROFILE_PICTURES_CONTAINER_NAME` | None; that file type fails to store |
| `AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME` | Literal `sim-execution-files`; set your intended name explicitly |
| `AZURE_STORAGE_OG_IMAGES_CONTAINER_NAME`, `AZURE_STORAGE_WORKSPACE_LOGOS_CONTAINER_NAME` | `AZURE_STORAGE_CONTAINER_NAME` |

Direct browser uploads require a Blob service CORS rule on the storage account. Allow your exact
Sim origin, `GET` and `PUT`, the `Content-Type` header, and the `x-ms-*` prefix used by signed blob
and metadata headers. Small-file uploads also send `If-None-Match`; the signature itself is
create-only, so a signed URL cannot overwrite an existing final object. Multipart completion verifies uploaded parts on the server:

```bash
az storage cors add \
  --services b \
  --methods GET PUT OPTIONS \
  --origins https://sim.yourdomain.com \
  --allowed-headers content-type if-none-match 'x-ms-*' \
  --exposed-headers ETag \
  --max-age 3600 \
  --account-name mystorageaccount \
  --account-key '<account-key>'
```

If you authenticate with a connection string, replace the last two options with
`--connection-string "$AZURE_CONNECTION_STRING"`. CORS is configured once for the account's Blob
service and applies to all of its containers.

A full Helm example lives at `helm/sim/examples/values-azure.yaml`.

## Set up Google Cloud Storage [#set-up-google-cloud-storage]

<Steps>
  <Step>
    ### Create the buckets [#create-the-buckets-1]

    GCS uses one bucket per purpose, mirroring the S3 layout:

    ```bash
    export PROJECT_ID=your-project-id
    export LOCATION=us-central1

    # The eight purpose-specific buckets. The CORS and IAM steps below reuse this list.
    export SIM_BUCKETS="workspace-files knowledge-base execution-files chat-files
                        copilot-files profile-pictures og-images workspace-logos"

    # Create buckets (names must be globally unique — prefix with your org)
    for name in ${SIM_BUCKETS:?run the export above first}; do
      gcloud storage buckets create "gs://myorg-sim-$name" \
        --project "$PROJECT_ID" \
        --location "$LOCATION" \
        --uniform-bucket-level-access
    done
    ```

    Keep all buckets **private** (no `allUsers` bindings). Uploads use short-lived V4 signed URLs and downloads are proxied through the app. Buckets do not need public read access.

    Because uploads are sent **directly from the browser** via signed `PUT` requests, each bucket needs a CORS policy that allows your Sim origin:

    ```bash
    cat > /tmp/cors.json <<'EOF'
    [
      {
        "origin": ["https://your-sim-domain.com"],
        "method": ["GET", "PUT"],
        "responseHeader": [
          "Content-Type",
          "ETag",
          "x-goog-if-generation-match",
          "x-goog-meta-uploadid",
          "x-goog-meta-originalname",
          "x-goog-meta-uploadedat",
          "x-goog-meta-purpose",
          "x-goog-meta-userid",
          "x-goog-meta-workspaceid",
          "x-goog-meta-knowledgebaseid",
          "x-goog-meta-folderid",
          "x-goog-meta-workflowid",
          "x-goog-meta-executionid"
        ],
        "maxAgeSeconds": 3600
      }
    ]
    EOF

    for name in ${SIM_BUCKETS:?run the export above first}; do
      gcloud storage buckets update "gs://myorg-sim-$name" --cors-file=/tmp/cors.json
    done
    ```

    Keep the explicit metadata and `x-goog-if-generation-match` headers in this policy: Sim signs create-only uploads, which prevent a reused URL from replacing existing bytes. Multipart completion reads provider-authoritative part state on the server; the browser does not need `ETag` for completion. See [GCS CORS configuration](https://docs.cloud.google.com/storage/docs/cross-origin).
  </Step>

  <Step>
    ### Grant access [#grant-access]

    Create a service account (or reuse the one your workload runs as) and grant it object access on the buckets:

    ```bash
    gcloud iam service-accounts create sim-storage --project "$PROJECT_ID"

    for name in ${SIM_BUCKETS:?run the export above first}; do
      gcloud storage buckets add-iam-policy-binding "gs://myorg-sim-$name" \
        --member "serviceAccount:sim-storage@$PROJECT_ID.iam.gserviceaccount.com" \
        --role roles/storage.objectAdmin
    done
    ```

    You then have two ways to supply credentials:

    * **Application Default Credentials (recommended on GCP)** — run Sim with the service account via GKE Workload Identity (or attach it to the GCE instance) and leave `GCS_CREDENTIALS_JSON` unset. Because there is no private key in this mode, generating signed URLs uses the IAM `signBlob` API — grant the service account `roles/iam.serviceAccountTokenCreator` **on itself**:

      ```bash
      gcloud iam service-accounts add-iam-policy-binding \
        "sim-storage@$PROJECT_ID.iam.gserviceaccount.com" \
        --member "serviceAccount:sim-storage@$PROJECT_ID.iam.gserviceaccount.com" \
        --role roles/iam.serviceAccountTokenCreator
      ```

    * **Inline key (for Docker Compose or non-GCP hosts)** — create a JSON key for the service account and set `GCS_CREDENTIALS_JSON` to its contents. With a private key present, signed URLs are generated locally and no extra IAM role is needed.
  </Step>

  <Step>
    ### Configure environment variables [#configure-environment-variables-1]

    ```bash
    # Credentials — omit both when using Workload Identity / ADC
    GCS_PROJECT_ID=your-project-id            # optional; inferred from credentials when unset
    GCS_CREDENTIALS_JSON='{"type":"service_account","client_email":"...","private_key":"..."}'

    # Buckets (per purpose)
    GCS_BUCKET_NAME=myorg-sim-workspace-files
    GCS_KB_BUCKET_NAME=myorg-sim-knowledge-base
    GCS_EXECUTION_FILES_BUCKET_NAME=myorg-sim-execution-files
    GCS_CHAT_BUCKET_NAME=myorg-sim-chat-files
    GCS_COPILOT_BUCKET_NAME=myorg-sim-copilot-files
    GCS_PROFILE_PICTURES_BUCKET_NAME=myorg-sim-profile-pictures
    GCS_OG_IMAGES_BUCKET_NAME=myorg-sim-og-images
    GCS_WORKSPACE_LOGOS_BUCKET_NAME=myorg-sim-workspace-logos
    ```

    `GCS_BUCKET_NAME` is what switches Sim into GCS mode and is the only bucket strictly required: every purpose-specific bucket falls back to it when unset. GCS bucket names are globally unique, so Sim uses no literal defaults here — not even for execution files. Set the dedicated buckets to separate the file types; leave them unset and everything lands in the general bucket.
  </Step>
</Steps>

### GCS bucket reference [#gcs-bucket-reference]

| Variable | Stores | Required |
| - | - | - |
| `GCS_BUCKET_NAME` | General workspace files | **Yes** (enables GCS) |
| `GCS_PROJECT_ID` | GCP project ID | No (inferred from credentials/ADC) |
| `GCS_CREDENTIALS_JSON` | Inline service-account JSON | No (uses Application Default Credentials if unset) |
| `GCS_KB_BUCKET_NAME` | Knowledge base documents | Recommended (falls back to `GCS_BUCKET_NAME`) |
| `GCS_EXECUTION_FILES_BUCKET_NAME` | Workflow execution files | Recommended (falls back to `GCS_BUCKET_NAME`) |
| `GCS_CHAT_BUCKET_NAME` | Deployed chat assets | Recommended (falls back to `GCS_BUCKET_NAME`) |
| `GCS_COPILOT_BUCKET_NAME` | Chat attachments | Recommended (falls back to `GCS_BUCKET_NAME`) |
| `GCS_PROFILE_PICTURES_BUCKET_NAME` | User avatars | Recommended (falls back to `GCS_BUCKET_NAME`) |
| `GCS_OG_IMAGES_BUCKET_NAME` | OpenGraph preview images | Recommended (falls back to `GCS_BUCKET_NAME`) |
| `GCS_WORKSPACE_LOGOS_BUCKET_NAME` | Workspace logos | Recommended (falls back to `GCS_BUCKET_NAME`) |

A full Helm example (Workload Identity, GKE) lives at `helm/sim/examples/values-gcp.yaml`.

## Set up S3-compatible storage [#set-up-s3-compatible-storage]

Sim works with any S3-compatible store by pointing the S3 client at a custom endpoint. Configure it exactly like AWS S3 (buckets, access key, secret), then add `S3_ENDPOINT` — and `S3_FORCE_PATH_STYLE` where the provider requires path-style addressing. The examples below cover [Cloudflare R2](https://developers.cloudflare.com/r2/), [MinIO](https://min.io/), and [RustFS](https://rustfs.com/). For other providers, including [Backblaze B2](https://www.backblaze.com/cloud-storage), verify signed uploads, multipart completion, and lifecycle behavior against that provider's current S3 support.

`S3_ENDPOINT` is trusted operator configuration. Sim accepts HTTP and private hosts without its outbound-request guard; do not derive it from untrusted input.

<Callout type="warn">
  **The endpoint must be reachable from your users' browsers, and the bucket needs CORS.** Uploads use presigned `PUT` requests sent **directly from the browser** to `S3_ENDPOINT` (downloads are proxied back through the app, so they only need server-side reachability). This means:

  * A purely internal endpoint (e.g. `https://minio.internal:9000` that only the app pods can resolve) will let the server start cleanly but **uploads will fail in the browser**. Use an endpoint your users can reach.
  * Configure a **CORS policy** on the bucket that allows your Sim origin with `PUT` and `GET`, and `AllowedHeaders: ["*"]`. With a custom endpoint, the browser sends the upload's metadata as signed `x-amz-meta-*` headers alongside `Content-Type` and `If-None-Match`, because many S3-compatible stores ignore metadata passed in the URL. If you list headers individually, include all of them. On MinIO, check the server-wide origin settings and any reverse proxy as well as the bucket policy; do not assume CORS is already configured.
  * Sim adds the `S3_ENDPOINT` origin (and its bucket subdomains, unless `S3_FORCE_PATH_STYLE` is set) to its Content Security Policy, so the browser may upload there.
</Callout>

<Tabs items="['Cloudflare R2', 'MinIO', 'RustFS']">
  <Tab value="Cloudflare R2">
    [Cloudflare R2](https://developers.cloudflare.com/r2/api/s3/) uses virtual-hosted style (the default) and the region `auto`:

    ```bash
    AWS_REGION=auto
    S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
    AWS_ACCESS_KEY_ID='<r2-access-key-id>'
    AWS_SECRET_ACCESS_KEY='<r2-secret-access-key>'
    S3_BUCKET_NAME=myorg-sim-workspace-files
    # ...remaining S3_*_BUCKET_NAME vars, one R2 bucket each
    ```

    Leave `S3_FORCE_PATH_STYLE` unset — R2 supports the default virtual-hosted addressing.
  </Tab>

  <Tab value="MinIO">
    For a [MinIO](https://min.io/docs/minio/linux/index.html) endpoint without bucket subdomain routing, use path-style addressing and the region configured by its operator. [Ceph RGW](https://docs.ceph.com/en/latest/radosgw/s3/commons/) supports both addressing styles when DNS is configured:

    ```bash
    AWS_REGION=us-east-1
    S3_ENDPOINT=https://minio.example.com   # must be reachable from users' browsers, not app-pods-only
    S3_FORCE_PATH_STYLE=true
    AWS_ACCESS_KEY_ID='<minio-access-key>'
    AWS_SECRET_ACCESS_KEY='<minio-secret-key>'
    S3_BUCKET_NAME=myorg-sim-workspace-files
    # ...remaining S3_*_BUCKET_NAME vars, one bucket each
    ```

    `http://` works server-side, but since the browser uploads directly to this endpoint, prefer a TLS endpoint your users can reach (a mixed-content `http://` target will be blocked on an `https://` Sim origin).
  </Tab>

  <Tab value="RustFS">
    For [RustFS](https://rustfs.com/), configure the endpoint, access key/secret, and region for your installation. This example uses path-style addressing:

    ```bash
    AWS_REGION=us-east-1
    S3_ENDPOINT=https://rustfs.example.com   # must be reachable from users' browsers
    S3_FORCE_PATH_STYLE=true
    AWS_ACCESS_KEY_ID='<rustfs-access-key>'
    AWS_SECRET_ACCESS_KEY='<rustfs-secret-key>'
    S3_BUCKET_NAME=myorg-sim-workspace-files
    # ...remaining S3_*_BUCKET_NAME vars, one bucket each
    ```

    The same browser-reachability and CORS requirements apply.
  </Tab>
</Tabs>

## Apply the configuration [#apply-the-configuration]

<Tabs items="['Docker Compose', 'Kubernetes (Helm)']">
  <Tab value="Docker Compose">
    Add the storage variables to the `.env` file used by `docker-compose.prod.yml`, then restart:

    ```bash
    docker compose -f docker-compose.prod.yml up -d
    ```

    New uploads now use the selected backend. Preserve any existing local files until you have migrated them; changing the storage configuration does not copy their bytes.
  </Tab>

  <Tab value="Kubernetes (Helm)">
    Set the variables under `app.env` (non-secret, e.g. region and bucket names) and supply credentials through a secret. The chart ships a complete example at `helm/sim/examples/values-aws.yaml`:

    ```yaml
    app:
      env:
        AWS_REGION: "us-east-1"
        S3_BUCKET_NAME: "myorg-sim-workspace-files"
        S3_KB_BUCKET_NAME: "myorg-sim-knowledge-base"
        S3_EXECUTION_FILES_BUCKET_NAME: "myorg-sim-execution-files"
        # ...remaining buckets
    ```

    On EKS, prefer **IRSA**: attach the IAM policy to the service account's role and leave the access-key variables unset.
  </Tab>
</Tabs>

## Configure incomplete multipart cleanup [#configure-incomplete-multipart-cleanup]

Sim uploads directly to a create-only final object key and keeps upload-session state in PostgreSQL.
The `/api/cron/cleanup-tasks` endpoint claims expired sessions before deleting uploaded objects or aborting provider multipart state. &#x2A;*Neither shipped scheduler calls this endpoint.** Configure an authenticated operator-owned schedule as shown in [Background jobs](/platform/self-hosting/background-jobs#schedule-upload-session-cleanup). Provider lifecycle cleanup is a second layer for multipart state that outlives its database row:

* On AWS S3 and Google Cloud Storage, abort incomplete multipart uploads after two days on every
  purpose-specific bucket.
* Azure automatically removes uncommitted blocks after seven days.
* For an S3-compatible provider, configure incomplete-multipart cleanup when its lifecycle
  implementation supports it. Check the provider's documentation because support varies.

The provider window should exceed the 24-hour upload-session lifetime so an in-progress completion
can still recover. Do not add an object-expiration rule for final upload keys.

<Callout type="warn">
  Object expiration and incomplete-multipart cleanup are different lifecycle operations. Configure
  the incomplete-multipart operation; expiring objects does not remove abandoned multipart parts.
</Callout>

## Verify storage [#verify-storage]

After restarting with the new configuration:

1. Open the app and upload a document to a knowledge base (or set a profile picture).
2. Confirm an object appears in the corresponding bucket/container.
3. Reload the page — the file should still render (downloads stream back through the app at `/api/files/serve`).

If uploads fail, check the app logs for credential or permission errors (see [Troubleshooting](/platform/self-hosting/troubleshooting)).

## Related docs [#related-docs]

* [Background jobs](/platform/self-hosting/background-jobs)
* [Troubleshooting](/platform/self-hosting/troubleshooting)
