Amazon S3
Publishing your tracking plan to an Amazon S3 bucket
Introduction
Avo can publish a snapshot of your tracking plan as a JSON file to an S3 bucket you control. This is useful when you want your tracking plan to be a versioned source-of-truth for downstream data tooling, dashboards, schema registries, or CI/CD checks. You can configure the integration to publish whenever a branch is merged or with a push of a button.
Amazon S3 publishing is currently in private beta. To request access for your workspace, reach out via the in-app support widget or at support@avo.app.
Configure the Amazon S3 integration
To enable the integration, navigate to the Publishing screen in your Avo workspace, click “Add Integration” and select “Amazon S3”. See details in our setting up and configuring a publishing integration docs.

You’ll configure the following fields:
| Field | Description |
|---|---|
| S3 Bucket Name | The bucket name only — no s3:// prefix and no region. |
| AWS Region | The region the bucket lives in. |
| Folder Prefix (optional) | A path segment placed in front of the workspace and branch folders Avo creates, e.g. avo-exports. |
| Avo’s service account ID | Read-only — the 21-digit ID Avo displays for you to paste into your trust policy accounts.google.com:sub condition. |
| Role ARN | The ARN of the IAM role Avo should assume, e.g. arn:aws:iam::123456789012:role/avo-s3-publisher. |
| Payload Format | The JSON shape Avo serializes. See Payload formats below. |
| Only send changed events | When on, the published payload only includes events whose schema changed on this branch, which is useful for incremental syncs. When off, every publish is a full snapshot. |
| Automatically publish on branch merge | When on, every branch merge triggers an automatic publish. When off, publishing only happens on a manual button click. |
Your bucket can live in any standard AWS region — GovCloud and China regions are not currently supported. Each integration writes to a single bucket; to publish to multiple buckets (e.g. dev, staging, prod), create a separate integration for each. Creating the IAM role behind the Role ARN and Avo’s service account ID fields is covered in Configuring AWS for Avo below.
Avo writes every object with SSE-S3 (AES256), set explicitly on each upload. Your bucket must allow SSE-S3 uploads — the default for new buckets. A bucket whose policy requires SSE-KMS (AWS-managed or customer-managed) will reject Avo’s uploads; SSE-KMS is not currently supported.
Configuring AWS for Avo
Avo authenticates to your bucket by assuming an IAM role in your account via OpenID Connect (OIDC) — it never holds any AWS credentials, not even encrypted. The mechanism is AWS’s built-in trust of Google’s OIDC issuer (accounts.google.com), so there’s no IAM identity provider to register in your account.
In the AWS IAM console (Roles → Create role → Custom trust policy), create an IAM role with two policies attached. First, a trust policy that lets Avo’s Google identity assume the role. From the Avo integration screen, copy Avo’s service account ID — a 21-digit number Google assigns to the service account (the JWT sub claim) — and paste it into the accounts.google.com:sub condition. The screen also shows the service-account email beneath the ID for context, so you know which account the ID belongs to; the email is not the value you paste.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "accounts.google.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"accounts.google.com:sub": "107886548342266542732"
}
}
}
]
}Second, a permission policy limiting the role to writing into your bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::<your-bucket>/*"
}
]
}The trust policy must use Principal: { "Federated": "accounts.google.com" }. If your organization’s policies forbid accounts.google.com as a federated identity provider, get in touch — we’ll discuss workarounds.
How a publish authenticates
Per publish, Avo’s Cloud Function:
- Fetches a short-lived, Google-signed JWT from the GCP metadata server. The JWT’s
subclaim is Avo’s service account’s unique 21-digit ID — a value only Google can populate. - Calls AWS STS
AssumeRoleWithWebIdentitywith that JWT and your Role ARN. - AWS validates the JWT signature against Google’s public JWKS and checks the
subclaim matches your trust policy condition — confirming the token was minted by Avo’s specific service account. It then mints temporary credentials valid for 15 minutes. - The Cloud Function uses those credentials to
PutObject; they expire automatically.
Avo never holds long-lived AWS credentials anywhere.
What gets written and where
Every publish writes two objects under the same prefix:
<folderPrefix>/<workspaceId>/<branchId>/<timestamp>-<publishType>.json ← immutable snapshot
<folderPrefix>/<workspaceId>/<branchId>/latest.json ← rolling pointer<timestamp>-<publishType>.jsonis the historical record of a single publish. The filename embeds an ISO-8601 timestamp and a short tag describing why the publish ran (ManualorBranchMerge). It’s never overwritten.latest.jsonis a rolling pointer scoped to its branch folder: it’s identical in content to the most recent timestamped file in that same folder, and is overwritten on every publish to that branch. A feature branch’slatest.jsontracks that feature branch; main’s tracks main. Downstream consumers that just want “the current tracking plan” should read thelatest.jsonin the folder they care about; consumers that need history can list the timestamped files.<workspaceId>is the Avo workspace ID. It’s stable and never changes.<branchId>is the Avo branch the publish came from — the branch is captured here in the folder path, not in the filename. Branches merged to main share a single main-branch ID; feature branches each have their own. This lets downstream tooling pin to a specific feature branch during development and switch to main once merged. A manual publish triggered from a feature branch lands in that feature branch’s folder; auto-publishes that run on branch merge always land in the main folder.- The object key layout is fixed: customizing the
<workspaceId>/<branchId>/<filename>segments (beyond the optional folder prefix) isn’t currently supported.
Example
With Folder Prefix = avo-exports and a publish from your main branch:
avo-exports/workspace_id/main/2026-05-29T14:23:11.000Z-BranchMerge.json
avo-exports/workspace_id/main/latest.jsonPayload formats
You can choose the JSON shape Avo serializes, each documented in detail on the Webhook payload format docs:
- JSON Schema — a representation of your tracking plan following the JSON Schema standard. This is the common pick for downstream validation.
- NDJSON — newline-delimited JSON, with one event or property object per line. Useful for streaming the file into row-oriented data tooling.
- Snowplow Schemas — a Snowplow Schema for every event, for integrating Avo with Snowplow Iglu. Selecting this format reveals a Vendor field where you set the Snowplow vendor value stamped into the schemas.
Publishing modes
- Manual — Click Publish on the integration page. Use this for one-off snapshots, or when you want a human in the loop before pushing schema changes downstream.
- Auto-publish on branch merge — Every time a branch is merged in Avo, a publish runs automatically. Because the merge target is main, these publishes always write to the main folder (
<workspaceId>/<mainBranchId>/), not a feature-branch folder. Use this if your downstream pipeline should always reflect the current production tracking plan with no manual step. See Configure Auto publishing for details.
Both modes write to the same files. The <publishType> segment in the timestamped filename tells you which one triggered the publish.
Security
- Avo holds no AWS credentials — not even encrypted. There’s no secret to store, log, or leak. Each publish mints a fresh Google-signed token (valid about an hour) and exchanges it for AWS temporary credentials that expire after 15 minutes.
- The service account that mints the token is dedicated to this integration and holds zero IAM roles in Avo’s GCP project — it can’t read Avo’s database, fetch secrets, or invoke other services. Your trust policy pins to this identity by its 21-digit
sub, so even a full compromise of the publish path yields only “write to opted-in customer buckets,” never a foothold inside Avo. - Scope the role to
s3:PutObjecton the Avo bucket only — avoidAmazonS3FullAccessor similar broad policies.
Why the trust policy conditions on sub, not aud
aud is whatever the caller requests when minting the token, so it isn’t an identity proof. AWS validates sub independently and the caller can’t set it — which is why the trust policy pins to Avo’s 21-digit service-account sub.
Failure handling
When a publish fails, the integration page shows a red “Published with errors” message and the activity log below the integration captures the specific failure. Avo writes the timestamped snapshot first and only updates latest.json after that write succeeds, so consumers reading latest.json always see a complete, consistent snapshot — never a partially written file.
The error message in the Avo UI surfaces the underlying AWS response. The most common failures and how to resolve them:
- 403 Access Denied: The assumed role doesn’t have
s3:PutObjecton the target bucket or folder prefix. Confirm the permission policy is attached and that itsResourceARN matches your bucket, e.g.arn:aws:s3:::<your-bucket>/*. - AccessDenied on
AssumeRoleWithWebIdentity: AWS refused to let Avo assume the role. Theaccounts.google.com:subcondition in your trust policy doesn’t match Avo’s service account ID shown in Avo, or the trust policy isn’t usingPrincipal: { "Federated": "accounts.google.com" }. Re-copy the service account ID from the integration screen into the condition. - InvalidIdentityToken: AWS couldn’t validate the Google-signed token (expired, clock skew, or a transient issue fetching Google’s JWKS). This is usually transient — click Publish again to retry.
- Role ARN errors (NoSuchEntity / malformed ARN): The configured Role ARN doesn’t exist or is malformed. Confirm it matches the role you created, e.g.
arn:aws:iam::123456789012:role/avo-s3-publisher. - 404 NoSuchBucket: The configured bucket doesn’t exist in the configured region. Check the S3 Bucket Name (name only, no
s3://prefix) and that the AWS Region matches where the bucket actually lives. - 301 PermanentRedirect: The bucket is in a different region than the one configured. Update AWS Region to the bucket’s real region.
- KMS / encryption errors: The bucket’s policy requires SSE-KMS. Avo writes objects with SSE-S3 (AES256), so a bucket that mandates KMS — whether AWS-managed or customer-managed — rejects the upload. Configure the bucket to allow SSE-S3 uploads; SSE-KMS is not currently supported.
- Network timeout: The request to AWS timed out. This is usually transient — click Publish again to retry.
- Other errors: The activity log captures the raw AWS error. If it isn’t one of the above, reach out via the in-app support widget or at support@avo.app.