> For the complete documentation index, see [llms.txt](https://docs.espresso.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.espresso.ai/snowflake-optimizer/snowflake-metadata/storage-integration-setup.md).

# Storage Integration Setup

Set up a Snowflake storage integration and external stage so Espresso can receive your usage metadata exports.

Set up a storage integration and named external stage in your Snowflake account so Espresso can write your usage metadata exports to Espresso's S3 bucket. No credentials are exchanged.

## Values

Find these on the **Proxy Onboarding** page of the Espresso dashboard:

| Value       | Example                           |
| ----------- | --------------------------------- |
| `<SLUG>`    | Your Espresso internal identifier |
| External ID | `<EXTERNAL_ID>`                   |

Your bucket path is `s3://apricot-snowflake-unload/prod/<SLUG>/`.

Information you need:

| Value                   | Notes                                              |
| ----------------------- | -------------------------------------------------- |
| `<YOUR_AWS_ACCOUNT_ID>` | The AWS account the IAM role will live in          |
| `<ESPRESSO_ROLE>`       | The Snowflake role your Espresso service user uses |

The steps below create an IAM role `EspressoUnloadRole`, a storage integration `ESPRESSO_UNLOAD`, and a stage `ESPRESSO.INTEGRATIONS.ESPRESSO_UNLOAD_STAGE`. Rename them if your conventions require it.

## Before you start

The integration needs the IAM role to exist, and the role's trust policy needs values that only exist once the integration is created. So the role is created with a placeholder trust policy in step 1 and finalized in step 4.

## Step 1 — Create the IAM role

```bash
cat > trust-placeholder.json <<'JSON'
{"Version": "2012-10-17", "Statement": [{
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:root"},
  "Action": "sts:AssumeRole"
}]}
JSON

aws iam create-role --role-name EspressoUnloadRole \
  --assume-role-policy-document file://trust-placeholder.json
```

## Step 2 — Attach the S3 permissions policy

```bash
cat > espresso-unload-access.json <<'JSON'
{"Version": "2012-10-17", "Statement": [
  {
    "Sid": "UnloadObjects",
    "Effect": "Allow",
    "Action": ["s3:PutObject"],
    "Resource": "arn:aws:s3:::apricot-snowflake-unload/prod/<SLUG>/*"
  },
  {
    "Sid": "ListPath",
    "Effect": "Allow",
    "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
    "Resource": "arn:aws:s3:::apricot-snowflake-unload",
    "Condition": {"StringLike": {"s3:prefix": ["prod/<SLUG>/*"]}}
  }
]}
JSON

aws iam put-role-policy --role-name EspressoUnloadRole \
  --policy-name EspressoUnloadAccess \
  --policy-document file://espresso-unload-access.json
```

**Send the role ARN to Espresso now.** We authorize it on our side, and that has to be in place before the step 6 check will pass.

## Step 3 — Create the storage integration

```sql
USE ROLE ACCOUNTADMIN;

CREATE STORAGE INTEGRATION ESPRESSO_UNLOAD
  TYPE = EXTERNAL_STAGE
  STORAGE_PROVIDER = 'S3'
  STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:role/EspressoUnloadRole'
  STORAGE_AWS_EXTERNAL_ID = '<EXTERNAL_ID>'
  STORAGE_AWS_OBJECT_ACL = 'bucket-owner-full-control'
  ENABLED = TRUE
  STORAGE_ALLOWED_LOCATIONS = ('s3://apricot-snowflake-unload/prod/<SLUG>/');
```

* `STORAGE_ALLOWED_LOCATIONS` pins the integration to that path.
* Use the External ID from the Proxy Onboarding page rather than letting Snowflake generate one, so it stays stable. If you later run `CREATE OR REPLACE`, keep the same value or step 4 has to be redone.
* `STORAGE_AWS_OBJECT_ACL` is required for the cross-account write.

## Step 4 — Finalize the role's trust policy

```sql
DESC STORAGE INTEGRATION ESPRESSO_UNLOAD;
```

Copy `STORAGE_AWS_IAM_USER_ARN` from the output.

```bash
cat > trust.json <<'JSON'
{"Version": "2012-10-17", "Statement": [{
  "Effect": "Allow",
  "Principal": {"AWS": "<STORAGE_AWS_IAM_USER_ARN>"},
  "Action": "sts:AssumeRole",
  "Condition": {"StringEquals": {"sts:ExternalId": "<EXTERNAL_ID>"}}
}]}
JSON

aws iam update-assume-role-policy --role-name EspressoUnloadRole \
  --policy-document file://trust.json
```

IAM changes are eventually consistent. If step 6 fails with `Access Denied`, retry shortly.

## Step 5 — Create the stage and grant access

```sql
CREATE DATABASE IF NOT EXISTS ESPRESSO;
CREATE SCHEMA IF NOT EXISTS ESPRESSO.INTEGRATIONS;

CREATE STAGE ESPRESSO.INTEGRATIONS.ESPRESSO_UNLOAD_STAGE
  URL = 's3://apricot-snowflake-unload/prod/<SLUG>/'
  STORAGE_INTEGRATION = ESPRESSO_UNLOAD;

GRANT USAGE ON INTEGRATION ESPRESSO_UNLOAD TO ROLE <ESPRESSO_ROLE>;
GRANT USAGE ON DATABASE ESPRESSO TO ROLE <ESPRESSO_ROLE>;
GRANT USAGE ON SCHEMA ESPRESSO.INTEGRATIONS TO ROLE <ESPRESSO_ROLE>;
GRANT USAGE ON STAGE ESPRESSO.INTEGRATIONS.ESPRESSO_UNLOAD_STAGE TO ROLE <ESPRESSO_ROLE>;
```

## Step 6 — Verify

```sql
USE ROLE <ESPRESSO_ROLE>;

COPY INTO @ESPRESSO.INTEGRATIONS.ESPRESSO_UNLOAD_STAGE/_connectivity_check/
  FROM (SELECT 1 AS OK)
  FILE_FORMAT = (TYPE = PARQUET)
  HEADER = TRUE;
```

A row count in the response means the integration, stage, trust policy, and authorization are all correct.

## Step 7 — Confirm with Espresso

Let us know the check in step 6 passed, and that the stage is `ESPRESSO.INTEGRATIONS.ESPRESSO_UNLOAD_STAGE`. We handle the rest.

## Troubleshooting

| Symptom                                         | Cause                                                                          |
| ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `Access Denied` on step 6                       | Trust policy still propagating, or Espresso hasn't authorized the role ARN yet |
| `SQL access control error` on the stage         | Missing `GRANT USAGE ON INTEGRATION`                                           |
| `Access Denied` after a working setup           | The integration was recreated and its External ID changed — redo step 4        |
| Files written but Espresso reports missing data | `STORAGE_AWS_OBJECT_ACL` omitted from step 3                                   |
