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

# Configure Async Exports Storage (Self-Hosted)

> Provision an S3 bucket so your self-hosted Opal instance can serve async data exports.

Self-hosted Opal stores async data exports (large CSV/ZIP downloads) in an S3 or S3-compatible bucket. Cloud customers get this bucket automatically. If you self-host, you provision the bucket and provide credentials.

The default path below is AWS S3. Opal also supports any S3-compatible object store — see [Alternative: Use Google Cloud Storage (GCS)](#alternative-use-google-cloud-storage-gcs) at the bottom for the GCS variant.

This setup is optional. If you skip these steps, exports still work, but they're tied to the browser session — they cancel if you navigate away or close the tab.

## 1. Create the bucket

Create a private bucket in the same region as your cluster, with:

* All public access blocked
* Server-side encryption (AES256 or KMS)
* A bucket policy that denies any request where `aws:SecureTransport=false`

Use a dedicated bucket per Opal deployment (e.g. `<your-org>-opal-exports`). Don't share it with other applications or other environments.

<Warning>
  Do **not** add an S3 lifecycle expiration rule. Opal runs its own file cleanup job, and should be the only thing deleting objects in this bucket. A lifecycle policy might cause previously-saved exports to be orphaned.
</Warning>

Versioning and CORS are not required. Downloads are served through the Opal backend, not directly from the browser.

## 2. Create an IAM user and access key

Opal authenticates to the bucket with a static access key pair. This is a two-step process: create a dedicated IAM user with a scoped policy, then issue an access key for that user.

### 2a. Create the IAM user

Create a dedicated IAM user (e.g. `opal-exports-service`) and attach a policy with **only** these permissions:

<CodeGroup>
  ```json policy.json theme={null}
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
        "Resource": "arn:aws:s3:::my-org-opal-exports/*"
      },
      {
        "Effect": "Allow",
        "Action": "s3:ListBucket",
        "Resource": "arn:aws:s3:::my-org-opal-exports"
      }
    ]
  }
  ```
</CodeGroup>

### 2b. Issue an access key

Generate one access key pair for the user (`aws iam create-access-key --user-name opal-exports-service`, or via the IAM console). Store both the access key ID and secret access key in your secret manager — you'll paste them in step 3.

If you provision the user via infrastructure-as-code, create the access key separately so the secret stays out of state. The secret access key is shown only once at creation time.

<Note>
  IAM Roles for Service Accounts (IRSA) and Workload Identity are not supported today. Static access keys are the only authentication method.
</Note>

## 3. Update Opal Configuration

Pick the section that matches your install method.

### KOTS

Open the admin console and find the **Async Exports Storage** section:

1. Click **Enable Async Exports**.
2. **Storage type**: leave as **AWS S3** (default).
3. Bucket name: `my-org-opal-exports`
4. Region: e.g. `us-east-2`
5. Access key ID and secret access key from step 2.

Save and deploy. KOTS encrypts the secret at rest.

### Helm

Add the following to your helm values:

<CodeGroup>
  ```yaml values.yaml theme={null}
  exportStorage:
    bucketName: my-org-opal-exports
    region: us-east-2
    accessKey: <access key id>
    secretKey: <secret access key>
  ```
</CodeGroup>

Then run a `helm upgrade` to apply the configuration.

## 4. Verify

Run a query in [Opal Query](/docs/opal-query) and trigger an export. You should get a download link and be able to download the exported data. Async exports are currently only supported via Opal Query.

## Alternative: Use Google Cloud Storage (GCS)

If you run Opal on GCP and would rather not provision AWS infrastructure, GCS works via its [interoperability mode](https://cloud.google.com/storage/docs/interoperability) — an S3-compatible XML API. The setup mirrors the AWS flow above with three differences:

* You create a GCS bucket instead of an S3 bucket.
* You generate an **HMAC key** for a service account instead of an IAM user access key.
* You point Opal at GCS' S3-compatible endpoint (`https://storage.googleapis.com`) instead of an AWS region.

### 1. Create the GCS bucket

In your GCP project, create a bucket with:

* **Location type**: Region (close to your cluster)
* **Access control**: Uniform (recommended)
* **Public access prevention**: Enforced
* **Encryption**: Google-managed (default)

Versioning is not required. GCS encrypts at rest and rejects plain HTTP by default — no extra policy needed.

<Warning>
  Do **not** add a Lifecycle rule that deletes objects. Opal runs its own cleanup job and should be the only thing deleting objects in this bucket.
</Warning>

### 2. Create a service account and HMAC key

1. **IAM & Admin** → **Service accounts** → create a dedicated service account (e.g. `opal-exports-service`). Skip the "grant access to project" step — we'll scope to the bucket instead.
2. **Cloud Storage** → your bucket → **Permissions** → **Grant access** → assign the service account the **Storage Object Admin** role on this bucket.
3. **Cloud Storage** → **Settings** → **Interoperability** → **Create access key for service account** → select your service account.
4. Save the **Access key** (starts with `GOOG1...`) and **Secret**. The secret is shown only once.

### 3. Update Opal Configuration

**KOTS:**

1. Click **Enable Async Exports**.
2. **Storage type**: select **S3-compatible (e.g. GCS)**.
3. **Bucket name**: your GCS bucket name.
4. **Endpoint URL**: `https://storage.googleapis.com`
5. **Access key ID** and **Secret access key**: from step 2.

Save and deploy.

**Helm:**

Use `endpoint` instead of `region`:

<CodeGroup>
  ```yaml values.yaml theme={null}
  exportStorage:
    bucketName: my-org-opal-exports
    endpoint: https://storage.googleapis.com
    accessKey: <GOOG1... HMAC access key>
    secretKey: <HMAC secret>
  ```
</CodeGroup>

Then run a `helm upgrade` to apply the configuration.

### 4. Verify

Same as the AWS flow: run a query in Opal Query, trigger an export, and confirm a `.zip` appears in your GCS bucket under `exports/<org-id>/<job-id>.zip`.
