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

# 00 audit

# Avala Cloud: S3 to R2 discovery audit

Date: 2026-09-08. Source: `origin/develop` at `6e2a2d99d5297bbd31c71edfa96e2088fb9f3508`, with recursively initialized pinned submodules. Working branch: `docs/r2-discovery-audit`.

**Status: source audit ready for review; live inventory and migration authorization pending. No infrastructure, application behavior, feature flag, object, credential or production configuration was changed. This report does not certify production parity or readiness to cut over.**

The first implementation slice is now narrowed, per the user's follow-up, to the default **Avala Cloud local-upload destination**. BYOB remains unchanged. The broader inventory below records later dependencies so that a small first PR cannot accidentally claim the entire storage migration is complete. A second requested surface is the reactivated `avala.ai/storage` marketing page. The user prefers `data.avala.ai` for data serving; earlier `datasets.avala.ai` references describe the initial proposal, not a reserved domain. A full update is authorized for the designated Slack thread after the implementation PRs are ready, not during this audit.

## 1. Findings that change the plan

1. **Local uploads use presigned POST, not PUT.** Server, browser and Python SDK depend on the POST form response. R2 requires a new upload protocol; changing the bucket or endpoint alone is insufficient. Preserve already-issued S3 grants and old clients.
2. **The quota premise differs from the code.** Defaults are 10 GiB per personal user and 100 GiB per organization. An existing transactional quota/reservation/prefix ledger should be extended, not replaced. The requested 100 GB per user is a policy change as well as a migration requirement.
3. **R2 support already exists as a connector.** It is not managed-storage routing, does not establish Avala ownership, and lacks the requested EU endpoint and checksum controls. Do not change BYOB to accomplish the managed default.
4. **Annotation history requires S3 versions.** Sessions and LiDAR restore/finalization read and persist VersionId. Current-key copy alone loses product history.
5. **Some inference handoffs require native S3.** SageMaker async input/output paths cannot be migrated by replacing a boto endpoint. Choose an invocation redesign or an explicitly approved temporary staging exception.
6. **Source preservation must cover existing deletion.** Purge workers, lifecycle expiry, noncurrent-version expiry and overwrite jobs can invalidate a soak window without migration tooling deleting anything.
7. **Public visibility does not mean downloadable public bytes.** The anonymous MCAP viewer deliberately uses bounded, watermarked slices. Raw open releases require separate publication eligibility and licensing review.
8. **Rollback has two meanings.** Returning new writes to S3 is a flag change. Serving R2-only objects during an R2 outage additionally requires verified S3 replicas and URL re-resolution or edge indirection. A flag alone cannot supply missing bytes.

The documentation checks and proposed treatments are in `notes.md` (in this folder; not in the site navigation); choices and rejected alternatives are in decisions.md (`reports/infrastructure/r2-migration/decisions.md`, internal repository inventory, not published here). The route inventory and draft copy are in marketing-proposal.md (`marketing-proposal.md`, in this folder; not in the site navigation).

## 2. Scope and evidence quality

This is Avala's Physical AI data loop: managed sensor ingest, visualization, labeling, derived artifacts and exports. Customer S3/GCS connectors and the dedicated read-only account `068760014274` are excluded. A dataset provider type, public flag, bucket name or accessible credential is not ownership proof. Each migration source needs an owner, account, class, jurisdiction and retained-generation manifest.

The whole-tree scan included hidden/ignored source, IaC, CLI instructions, workflows, SDKs, generated files, tests, historical reports and pinned submodules. It excluded credentials/environment files, Git internals and installed/build caches. It covered `boto3`, `aws-sdk`, `@aws-sdk/client-s3`, `S3Client`, `s3://`, presigning, multipart, S3/CloudFront domains, bucket bindings, AWS region settings, IAM, Terraform/CDK/Pulumi/CloudFormation, event/SQS references, lifecycle, Inventory/Storage Lens, CORS and CLI copies/syncs.

The scan found **2,756 broadly matching files / 22,573 lines**, including **1,177 files with storage-specific signals**. These are search candidates, not a count of production calls. Generic lifecycle/CORS matches, non-storage boto clients, vendored examples and historical plans are not migration targets.

Detailed internal evidence accompanies this report:

* Server/API touchpoints (`reports/infrastructure/r2-migration/api-touchpoints.md`, internal repository inventory, not published here)
* Browser, SDK, MCP, Sessions and pipelines touchpoints (`reports/infrastructure/r2-migration/clients-touchpoints.md`, internal repository inventory, not published here)
* Infrastructure, bucket and compliance evidence (`reports/infrastructure/r2-migration/infra-touchpoints.md`, internal repository inventory, not published here)
* Per-file search index (`reports/infrastructure/r2-migration/storage-search-index.tsv`, internal repository inventory, not published here) and search methodology (`reports/infrastructure/r2-migration/search-method.md`, internal repository inventory, not published here)

The internal appendices preserve customer-specific source paths for review. This requested migration directory is not added to public documentation navigation. Do not publish the internal appendices as marketing copy.

**Live observations:** STS confirmed the local `avala-prod` profile targets account `014424401899`. No bucket/object listing, billing query, object download, histogram scan, benchmark, Vanta tenant query or Terraform plan was run. A separate approval request for a read-only AWS inventory capped at \$5 is pending. Root/management and development account ownership, deployed versions and actual bucket bindings still need validation.

**Unknown is not zero.** Current GB, object counts, histograms, active object versions, delete markers, multipart uploads, live lifecycle/CORS/policies, actual CloudFront mappings, EU commitments, exact Vanta tenant mappings and recent billing remain unverified. No absolute migration cost can responsibly be certified from source alone.

## 3. Avala-owned bucket inventory candidates

These are IaC declarations or explicit operational references. Verify ownership and existence before use. `{env}` and `{stage}` are real deployment substitutions, not literal bucket names. Region `provider` means `var.region`, not a verified live location. **For every row, current GB, current object count and size histogram are unknown.** `U` means no explicit control found in the reviewed declaration, not disabled in production.

| Bucket / family | Region evidence | Lifecycle and versioning in source | Access, encryption, replication/CDN | Class / scope |
| - | - | - | - | - |
| `avala-customer-dataset-uploads-production` | us-east-1 comment, `infra/s3.tf:807,886` | Abort incomplete MPU 7 days; versioning Suspended; no current expiry | AES256, all public blocks, POST/GET/HEAD CORS with ETag and range response headers | First target: user-managed |
| `avala-customer-datasets-local-upload` | Root account, us-west-1; `infra/s3.tf:813` | Live settings unknown; historical comment says about 5.6M objects | Outside this Terraform workspace | Legacy user-managed and derived; preserve source |
| `avala-server-media-{env}` | provider; `infra/s3.tf:106` | Noncurrent expiry 30 days, MPU abort 7 days; prod versioning | Public GET on allowlisted prefixes only; POST/GET CORS; prod cross-region replica | Media/derived, mixed private and public |
| `avala-server-exports-{env}` | provider; `infra/s3.tf:410` | Expiry 90 days; prod versioning | Private, HTTPS policy, prod replica | Derived exports |
| `avala-external-field-{env}` | provider; `infra/s3.tf:701` | Versioned; noncurrent expiry 90 days, MPU abort 7 days | AES256, private, GET/HEAD CORS | Annotation payloads, derived |
| `avala-data-collection-{env}` | provider; `infra/s3.tf:1140` | Suspended versioning; MPU abort 7 days | AES256, private, POST/GET Range CORS | Managed capture recordings |
| `avala-rc-qc-submissions-production` | provider; `infra/rc_qc_submissions.tf:35` | U lifecycle/versioning | Private, owner enforced, AES256, HTTPS | QC-derived |
| `sam3-async-input`, `sam3-async-output` | provider; `infra/sagemaker.tf:27` | Versioned; prefix expiry 7/30 days | Private AES256, native SageMaker S3 coupling | Inference tmp/derived |
| `bbox-refinement-async-input-{env}`, `bbox-refinement-async-output-{env}` | Nonprod names, same IaC | Same preceding controls | Same preceding coupling | Inference tmp/derived |
| `image-to-3d-input-{env}`, `image-to-3d-output-{env}` | provider; `infra/image_to_3d.tf:23` | Input 7 days, output 90 days; U versioning | Private AES256, native SageMaker S3 coupling | Inference tmp/derived |
| `sam3-async-failure` | us-east-1 comment; `infra/s3_https_only_external.tf:21` | Unknown | Only HTTPS policy managed here | Inference failures |
| `avala-session-storage-{stage}` | Serverless deployment region; `sessions/sessions/serverless/resources/s3.ts:1` | Expiry 35 days; U versioning | AWS KMS customer key and bucket key | Session tmp/state |
| `avala-lidar-phase-1-files-production`, `-development` | Production deployment references us-east-1 | Manual bucket; application requires versions | Manual ObjectCreated notification; commented version/CORS reference is not live proof | Annotation history, derived |
| `avala-public-datasets` | Historical us-west-1 runbook | Unknown | Runbook references public-read and CloudFront; distribution unknown | Public downloadable assets, eligibility required |
| `avala-public-datasets-development` | Historical plan only | Unknown | Existence/ownership not live verified | Development public data |
| `avala-public-assets` | `infra/s3.tf:348` reference, no root declaration | Unknown | Learning upload destination reference | Public learning media |
| `avala-db-dumps-{env}` | provider; `infra/s3.tf:2` | Glacier after 30 days, expiry 365; prod versioning | Private, prod replica | Backup scope decision; no retention changes |
| `avala-db-dumps-production-replica`, `avala-server-media-production-replica`, `avala-server-exports-production-replica` | us-west-2; `infra/s3_replication.tf:55,77,99` | Versioned; delete-marker replication | STANDARD\_IA replica storage, HTTPS | DR copies; avoid double-counting logical assets |
| `avala-alb-logs-{env}` | provider; `infra/s3.tf:577` | Expiry 90 days | Private owner-enforced, AWS log delivery | Operational, native AWS boundary |
| `avala-alb-logs-production-014424401899` | Historical us-east-1; external-policy IaC | Unknown | Legacy log policy only | Operational historical |
| `download.{domain}` | provider; `infra/s3.tf:472` | MPU abort 7 days; U versioning | Public GET plus CloudFront at line 532 | Installers/software distribution scope |
| `avala-static-assets-{env}` | provider; `infra/static_assets.tf:41` | Versioned, retain five noncurrent versions after 30 days | AES256, private CloudFront OAC | Application bundles, separate scope |
| `segment-anything-model-artifacts-prod` | us-west-1; external-policy IaC | Unknown | HTTPS policy only in source | Model artifacts scope |

Historical metrics are not current inventory: the March cost report records 179 GB for the LiDAR phase-1 bucket and 6.4 GB for server media (`reports/infrastructure/COST_SAVINGS_ANALYSIS_2026_03.md:248`). Do not extrapolate today's managed total from those values or from the 5.6M-object comment.

For each live bucket collect current/noncurrent bytes and counts separately; incomplete multipart bytes; size bins `<64 KiB`, `64 KiB-1 MiB`, `1-5 MiB`, `5-100 MiB`, `100 MiB-1 GiB`, `1 GiB-1 TB`, `>1 TB`; storage class and restore status; lifecycle rule filters; versions/delete markers; replication; encryption/key custody; access policy/public blocks; CORS; inventory/Storage Lens configurations; event destinations; CloudFront origins/aliases; owner and residency. Prefer an existing Inventory export for exact histograms. Daily bucket metrics cannot reconstruct a histogram or tenant ownership. Full list/version scans or export reads require an approved request budget.

## 4. Upload, quota and conversion paths

| Source | Bucket / binding | Operations | Caller | Size profile, not a measured histogram | User-facing |
| - | - | - | - | - | - |
| `server/server/apps/dataset/manual_upload_utils.py:91,198` | Manual-upload setting | Presign POST | API for browser/SDK | 1 byte to 5 GiB default | Yes |
| `mission_control/lib/datasets/add_items/item_sources/local_file_upload/repository/local_upload_repository.dart:76,118,223` | Server-returned target | Form POST then explicit dataset create | Browser | Streams file, four concurrent files; no MPU ID | Yes |
| `sdks/python/avala/resources/datasets.py:322,862,881` | Server-returned target | POST, dataset create, file-level retry/resume | SDK/CLI | Streamed file, no fixed-part checkpoint | Yes |
| `server/server/apps/fleet/api.py:552,704`; `sdks/python/avala/resources/fleet/uploads.py:122,211,240` | UploadSession/StorageConfig or server fallback | Presign PUT, complete with ETag | API/device/SDK/CLI | Recording files; mixed owned/customer binding | Yes |
| `server/server/apps/dataset/utilities/data_collection_files_storage.py:51,127,259` | Capture bucket | POST/get presign | Browser/mobile/API | Videos/MCAP, 5 GiB default cap | Yes |
| `server/server/apps/core/utils.py:578`; avatar upload cubit | Server media | POST; Django storage FileField operations | Browser/API | Avatar/media images | Yes |
| `server/server/apps/dataset/api_access.py:424,510,616` | User/org manual prefix | Reserve, presign, list, settle | API | Client-declared and observed bytes | Yes |
| `server/server/apps/dataset/quota.py:117,285,950,999`; `dataset/tasks.py:1130,1221,1340` | Single global manual bucket/prefix | List, reconcile, purge/delete | API/Celery | Entire prefix; request cost depends on count | Yes |
| `pipelines/shared/mcap_extractor/s3.py:21,64,76`; `shared/ingest_push/push.py:49,72,123` | Caller destination / legacy managed bucket | Managed multipart upload, manifest download, API register | Worker | Extracted images, point clouds, ALF and manifests | Indirect |
| `pipelines/converters/mcap/batch_common.py:56,104,274` | Caller source/archive/destination | CLI sync and listed-size verification | Worker | Converted MCAP/raw archives | Indirect; ownership-dependent |

### Existing quota behavior

`server/server/settings/common.py:1649,1652` defines 10 GiB per user and 100 GiB per org. `OwnerUploadQuota`, `OwnerUploadReservation` and `OwnerUploadPrefix` already track usage, reservations, legacy floors, owner prefixes and reconciliation state.

Presign uses row locks and reserves declared bytes. Excess returns HTTP 413 with usage/limit detail; inability to measure safely returns 503. S3 POST policy currently enforces MIME and content-length-range. Grants last one hour and reservations are retained 24 hours. R2 PUT must replace the POST size-policy guarantee deliberately: reservation admission, validated completion, quarantine of oversized/uncommitted objects, and a hard upload-size mechanism tested against actual signed headers or an enforcing gateway. CORS is not authorization or quota enforcement.

Finalize lists the prefix, rejects confirmed emptiness with 409, saves the Dataset, then settles reservations best effort. It is not yet the requested atomic verified object record + final quota check + enqueue transaction. Nightly reconciliation scans selected active/incomplete/stale owners, not all idle tenants. Archive is quota-neutral; seven-day delayed purge credits confirmed deletion. All of these race protections must survive provider/class expansion.

The browser already renders server usage and maps 413 to quota exceeded. Distinct 80/95 percent warnings are absent. Derived bytes written under the same prefix count implicitly; external-field, exports, capture and inference buckets do not participate in a unified managed allowance. Proposed policy: separately metered and bounded org-derived allowance, explicitly approved before activation. Do not silently change the current org cap when offering 100 GB/user.

### Conversion trigger findings

Local finalize saves through the dataset serializer and schedules indexing. MCAP thumbnail/proxy generation reads/writes through the dataset client (`dataset/mcap/tasks.py:574,657,698`); ALF/extraction pipeline uploads and registration are separate worker paths. This is useful for explicit completion dispatch, without inventing an S3 event dependency.

No automatic ROS bag/db3 -> MCAP conversion-on-local-upload implementation was found in the reviewed server/local-upload paths. Pipelines contain ROS decoding, MCAP extraction and format-specific converters, plus source-specific bag inspection. Those do not establish the requested automatic conversion job. Record its actual deployed owner/trigger or implement it as a separate approved feature. A 5 GB+ end-to-end acceptance flow cannot be claimed from existing code or a mock test.

## 5. Read, serve, SDK and MCP paths

| Source | Bucket / binding | Operations | Caller | Size profile | User-facing |
| - | - | - | - | - | - |
| `server/server/apps/dataset/clients/dataset_client_s3.py:370,566,819,851,883,926,944` | Persisted provider config, managed or BYOB | Get/range/head/list/put/presign; CloudFront branch | API/workers | Sensor corpus, multi-GB possible | Yes |
| `server/server/apps/dataset/mcap/api.py:1635,1665,1687,2246` | Dataset client/eligible proxy bucket | Head, presign or capability grant | API/browser | MCAP ranges | Yes |
| `packages/mcap_visualizer/lib/src/mcap/file_provider/remote_file_provider.dart:178,213,288,335` | Issued URL | Range/suffix GET, one-byte probe, cache warm | Browser | Bounded windows, optional bounded whole-file warm | Yes |
| `asset_proxy/app/main.py:116,193,227` | Signed bucket/key capability | S3 presign and streaming conditional/range proxy | ECS proxy | Sensor/media stream | Yes |
| `server/server/apps/dataset/utilities/lidar_phase_1_files_storage.py:117,169,225,241,274` | LiDAR phase-1 bucket | Presign/list versions/copy/delete exact version | API/annotator/workers | Annotation and ground/point-cloud artifacts | Yes |
| `server/server/apps/core/external_field.py:43,83,123,148,175` | External-field setting | Put/get/delete/presign | Models/API/workers | JSON/gzip masks and annotations | Yes |
| `server/server/apps/export/tasks.py:289`; `export/converters/avala.py:41`; `export/serializers.py:203` | Server exports | smart\_open put, presign get | Worker/API/SDK | Export archives/JSON | Yes |
| `sdks/python/avala/datasets.py:750`; `avala/_resolver.py:385,881` | Public revision manifest/access grant | Anonymous resolve/get/size/SHA-256 verification | SDK | Whole-object spooling, not proof of Range | Yes |
| `sdks/typescript/packages/mcp-server/src/tools/exports.ts:54`; `src/assetHandles.ts:759,999,1128` | REST-issued asset | Resolve opaque handle, recheck API access, URL release | MCP | Metadata/capabilities | Yes |
| `server/server/apps/dataset/device_id_resolver.py:82` | Dataset prefix | Bounded manifest get via DatasetClient | API/ingest | Device CSV | Indirect |

Private MCAP presigns commonly last 24 hours; generic dataset URLs default to seven days. Proxy grants are approximately 60 seconds. MCAP's immutable cache header is lifetime-bound, with a 300-second fallback on failed HEAD. The proxy emits `private, no-store`. Authenticated OPFS caches are encrypted and scoped to user plus content/ETag identity; anonymous viewer cache scope is null. Preserve Range, Content-Range, ETag, If-Range and conditional headers, and measure cache invalidation when multipart ETags change. Do not globally apply a year-long private edge cache policy.

Python `_uploads.py:39,99` rejects R2 and arbitrary custom upload hosts today. The public resolver separately knows R2 API endpoints but rejects the proposed `datasets.avala.ai`. Extend exact trusted origins and test redirects/SSRF boundaries. Existing `avala.load` should gain a supported downloadable manifest origin, not a duplicate loader.

`dataset/mcap/public_api.py:56,133,164,173` forbids raw URLs and requires bounded watermarked slices with no-store responses. Catalog seeding defaults to external links and does not copy bytes. Keep downloadable open releases separate from catalog visibility and public previews. Use an immutable revision manifest and an explicitly eligible release for the anonymous one-line load.

MCP already uses opaque handles and current authorization checks before releasing a URL. Preserve that contract and never introduce bucket credentials or persistent private URLs in tool responses.

## 6. Workers, events and external handoffs

| Source | Bucket / binding | Operations and trigger | Caller / size profile | User-facing |
| - | - | - | - | - |
| Sessions `adapters/storage/index.ts:17` and `api/*` | STORAGE\_BUCKET / annotation bucket | Put, managed MPU, get/version get, head, list, delete | Lambda; JSON/session/label streams | Indirect |
| Sessions `serverless/resources/lidarAnnotationSubmissionQueue.ts:23`; submission consumers | Manual LiDAR bucket | S3 ObjectCreated -> Lambda -> SQS -> merge; VersionId persisted | Annotation worker; compressed multi-frame labels | Yes |
| `server/server/apps/inference/utils.py:54,257,264`; `infra/sagemaker.tf:417` | SAM3 input/output | Put + native async s3:// handoff | API/SageMaker; request JSON/masks | Yes |
| `server/server/apps/image3d/utils.py:17,79,104`; `image3d/tasks.py:67,84,91`; `sagemaker/image-to-3d/serve.py:150,220` | Image-to-3D input/output | Upload/get/put/presign, native SageMaker output | API/SageMaker; up to 20 MB input image, meshes | Yes |
| `sam3_benchmark/sagemaker_async/sam3_model.py:72`; `yolo_model.py:138`; notification Lambda | Configured model/input/output bucket | List/get model weights and failure/result reads | Inference worker/notification Lambda | Indirect |
| `pipelines/core/remote_storage.py:163`; `core/s3_utils.py:113`; `ops/aws_sync.py:141,189` | Caller URI/profile | aws CLI sync/cp, HeadBucket, rigid URL conversion | Worker/operator; arbitrary corpus | Ownership-dependent |
| `pipelines/site/src/lib/aws/mcap-s3.ts:26,40,106,124,161,181` | Legacy managed bucket, us-west-1 | Presign/get/head/list/put | Pipeline API/browser; MCAP/extracted files | Yes |
| Vendor-delivery import scripts, exact paths in the internal client inventory (`reports/infrastructure/r2-migration/clients-touchpoints.md`, internal repository inventory, not published here) | Legacy managed output; vendor source is distinct | CLI ALF sync then aws\_s3 dataset registration | Vendor-delivery ingest; imagery/ALF/splats | Yes |
| `pipelines/converters/worlds/reconstruct.py:233`; `register_splat_dataset.py:62` | Caller destination | Recursive CLI put and S3 dataset registration | Reconstruction worker; splats/metadata | Indirect |
| `pipelines/ops/google_docs_to_s3.py:71,123,131`; `copy_practice_images.py:93,101,139` | Managed practice/media | Get/put, public-read ACL, rigid S3 URLs | Content tooling; images | Yes |
| `.github/workflows/pose-reconstruction.yml:429,475,494,496,745,774` | Input-derived bucket | Head/get/copy/delete/put; backups and overwrite | GPU workflow; MCAP/pose sidecars | Ownership-dependent |

Customer and vendor sources are not owned output buckets. The internal pipeline table records these boundaries without advertising customer relationships here. The existing direct-client guard must include CLI subprocesses and framework storage backends, not just constructors. Non-S3 boto use in Sessions SDK, local filesystem search indexes, and local reconstruction staging are not storage migration work.

No separate storage-backed Slices/AutoTag or LeRobot export path was confirmed by name in the reviewed source. Dataset item metadata and generic exports are present; obtain the active job definitions and deployed producer/consumer map before claiming complete pipeline coverage. EC2 runtime scripts, external vendor services and scheduled jobs can exist outside the checkout. Multi-GB throughput and NAT/proxy processing costs were not measured.

## 7. Infrastructure and compliance couplings

Root Terraform already configures Cloudflare, pinned to `~> 4.0` in `infra/versions.tf:17`. R2 resources may require a separately reviewed provider upgrade. Existing state is remote; no init/plan/apply was run. Runtime bucket bindings are injected into server, scheduler, chunk-worker and worker templates, including a literal S3 external-field base URL (`infra/ecs_task.tf:394,395`).

No active Inventory or Storage Lens resource was identified in the reviewed root IaC. Live presence is unknown. Manual LiDAR event notification is explicitly documented outside active IaC and retained Lambda permission proves a dependency. Current CloudFront declarations cover download/static origins; public-dataset distribution identity remains unknown. Cloudflare CDN in front of S3 is not R2.

The `infra/modules/customer_aws_bridge/` boundary and customer credential flows remain unchanged. The managed asset proxy has a deliberately narrow read-only IAM role and cannot simply be given broad bucket access to compensate for missing ownership metadata.

| Vanta evidence | Specific source | Required remapping / unknown |
| - | - | - |
| `aws-storage-buckets-enforce-https` | `infra/s3.tf:1039,1258`; `infra/sagemaker.tf:187` | R2 TLS evidence, bucket asset mapping; verify actual tenant test assignment |
| `aws-s3-bucket-server-side-encryption-enabled` | `security/VANTA.md:46` | Documented example ID, not verified active tenant test; R2 encryption/key-custody evidence |
| Lifecycle policy enforcement on sensitive S3 buckets | `infra/s3.tf:384` | Preserve approved retention; exact Vanta tenant ID unknown |
| Replication/DR requirement | `infra/s3_replication.tf:2` | Replace recovery evidence, including retained generations; exact test ID unknown |
| Inventory items have active owners | `infra/rc_qc_submissions.tf:8` | Owner/classification fields for R2 assets; exact test ID unknown |
| Vanta data-classification tags | Several bucket resources | Reconcile tags with actual user content; image-to-3D output currently marked no user data despite generated meshes |

No live Vanta tenant test export or signed EU/DPA commitments were available in this audit. These are explicit blockers to declaring compliance complete, not invented test IDs. Existing Sessions AWS KMS and S3 backup Glacier controls require a reviewed replacement or scoped exception.

Proposed legal/evidence diff, not applied: update `reports/compliance/system-description.md` AWS/S3 encryption, retention and vendor sections; add Cloudflare's object-storage purpose and jurisdictions to reviewed subprocessor disclosures; retain AWS compute/database/backup scope where still used. `marketing/content/legal/privacy-policy.mdx:95,143` references AWS and US hosting; the subprocessor page uses localized legal keys. The user handles customer notices. EU storage routing must cover derived/tmp and fallback, and contracts must separately permit the location of compute processing.

## 8. Proposed PR sequence and approval contract

Every implementation PR includes changed behavior, tests, rollback, and `Knowledge encoded:`. Flags are per org and class, default off, with Avala internal as canary. No production flip, paid integration test, Terraform application, migration batch or teardown is authorized by this report. Narrowing the first slice does not remove compatibility prerequisites.

| PR | Concrete change | Verification before promotion | Reverse |
| - | - | - | - |
| 0 | This audit, notes, decisions, internal inventory | Source-link/path checks, no secrets/customer copy in requested public-path docs, no em dashes, docs-only diff | Remove/revert documentation; no runtime effect |
| 1A | Managed ownership/locator contract; default-off org/class allocation; pin upload origin; retain S3 adapter behavior | Existing S3 behavior and BYOB contract tests; allocation vs persisted-locator tests | Flags off; retain additive schema/read support |
| 1B | Harden Python R2 managed driver and quota/reservation integration; protocol-aware PUT/MPU completion/outbox | S3 tests plus approved real R2 dev suite: put/MPU/abort/range/head/list/copy/delete/presigns/quota/EU; concurrent completion and failed transactions | New allocations S3, existing upload completion follows original locator |
| 1C | Browser and Python SDK local-upload support; retain POST for old clients; exact R2 origin validation | Browser and SDK S3 POST + R2 PUT/MPU, retries/resume, size/quota errors; fixed parts | Server stops issuing R2 grants to new uploads; old grants still complete |
| 1D | Approved R2 buckets/secrets/CORS configuration for local-upload canary; Terraform proposal separated from apply | Reviewable plan with safe artifact redaction; canary 5 GB+ upload/read and rollback rehearsal after approval | Allocation flag back; retain R2 data and grants; no destroy |
| 2 | Extend protected reads, artifact locators and derived quota; MCAP conversion/ALF job gaps separately identified | Range/CORS/cache/auth tests; upload -> processing -> viewer -> annotate -> export | Disable new R2 allocation per class; preserve generation-bound reads and replicas |
| 3 | Reactivate `/storage` as Avala Cloud with accurate availability, localized copy/metadata and routing | i18n scan/unused, lint/build/locale SEO, route tests and browser review | Restore prior redirect/navigation; no storage configuration effect |
| 4 | Sessions TS provider, immutable annotation revisions and R2 event bridge; migrate worker/CLI managed paths | Historical restore, duplicate events, retries, vendor boundary and dual-driver tests | Source routing by generation; retain old event path until drained, avoid duplicate dispatch |
| 5 | Explicitly downloadable public releases and existing anonymous SDK loader at the preferred `data.avala.ai` origin | Stable manifest/hash and Range tests; private/preview negative-access tests | Resolve eligible release to verified S3 replica, preserve published immutable URLs through edge routing |
| 6 | `tools/storage-migrate/`, manifest verification, cost guardrails, observation/rollback runbook | Dry-run/resume/idempotency/collision/tombstone tests; separately approved batches; parity evidence | Stop new migration work, disable approved Sippy config, restore allocation flags; no source deletion |
| 7 | Compliance evidence, cost dashboards, seven-day quota/parity soak, canary outage rehearsal | Count/bytes/metadata/hash evidence, ≤0.1% quota drift for seven days, throughput and restore evidence | Config-only operational rollback using verified replicas and URL refresh |
| 8 | Separately approved S3 teardown and obsolete credential retirement | Explicit retained-history/backup acceptance, soak and rollback sign-off | Teardown ends the cheap S3 fallback; require a new recovery plan before approval |

Stack only dependent contract/client layers. Marketing and independent infrastructure proposals can be separate reviews. User requested a full Slack update after the infra/server/Mission Control implementation PRs are complete; include all links if delivered as a stack. Nothing is posted yet.

## 9. Proposed verification and rollback requirements by phase

These are proposed controls, not runnable commands or deployed flag names. Exact commands must be written against the implemented configuration service in the implementation PRs. No current runtime flag can safely perform the complete migration today.

| Phase | Verify | Reverse prerequisite and action |
| - | - | - |
| 0 discovery | Evidence completeness and explicit unknowns | No runtime reversal needed |
| 1 abstraction | Both providers and BYOB boundary; locator immutable through retries | Set new allocation to S3; keep persisted reads and schema |
| 2 IaC | Reviewed plan, private access, jurisdiction, secrets scope | Disable new routing; leave unused resources intact; do not destroy |
| 3 upload | In-flight S3 POST, fixed MPU, quota, completion idempotency | New S3 grants; honor old R2/S3 upload IDs until complete/expiry |
| 4 serve | Range, cache isolation, expired/revoked grant, public eligibility | Re-resolve or edge-route to same-generation verified replica; do not rewrite signed hostnames |
| 5 quota | Active and idle tenants, originals/derived, delete races, seven-day drift | Same ledger across providers; stop new R2 allocations, not accounting |
| 6 migration | Count/current+referenced-history bytes, metadata, collision map, hashes, skipped records | Stop batches/Sippy only through approved controls; retain both copies/tombstones |
| 7 pipelines | End-to-end jobs, event replay, external vendor delivery, EC2 throughput | Per-class allocation reversal and generation-bound inputs; drain duplicate events |
| 8 security | Encryption/DR/residency evidence and legal approval | Retain old evidence and credentials during soak; no premature retirement |
| 9 observability | Storage/classes/ops/retrieval, quota, presigns, upload success, p95 first byte, Sippy misses | Keep dual-provider dashboards and alerts active during reversal |
| 10 rehearsal | Internal org actually exercises restore and URL refresh | Full outage rollback only after verified replicas; otherwise record unmet acceptance |

ETag/size/count parity is a useful gate, not a universal content proof. Multipart layouts and KMS objects make ETags unsuitable as universal hashes. A sampled hash adds evidence, not certainty for every unsampled byte. Propose capture of a cryptographic digest during the first authorized transfer and destination verification where practical, with explicit sampling coverage and residual uncertainty in `06-verification.md`. Budget source re-reads separately. Do not create a verification document that implies migration has occurred before it does.

## 10. Ranked risks

| Rank | Severity | Risk and release gate |
| - | - | - |
| 1 | Critical | Mixed BYOB/managed ownership and incomplete owned-account inventory. Require verified source registry and immutable locators before allocation changes. |
| 2 | Critical | Version history, overwrite races, lifecycle expiry, purge and NFC key collisions can lose or resurrect data. Require retained-generation manifest, tombstones and approved preservation controls. |
| 3 | Critical | R2-only writes have no guaranteed S3 outage fallback. Require verified replicas plus URL refresh/edge indirection before claiming zero-loss config rollback. |
| 4 | Critical | Endpoint-only switch breaks POST clients, size policies and in-flight upload finalization. Require dual-protocol API/client deployment before enabling R2. |
| 5 | Critical | Custom-domain publication could bypass private proxy or bounded public preview. Require explicit downloadable eligibility and auth/cache negative tests. |
| 6 | Major | Native SageMaker S3 and manual S3 event integrations remain. Require invocation/event design, not hidden exceptions. |
| 7 | Major | Quota baseline differs, finalization is not atomic, idle reconciliation is selective, derived coverage incomplete. Extend existing ledger and decide 100 GB policy explicitly. |
| 8 | Major | EU jurisdiction, KMS replacement and Vanta/legal mappings lack live evidence. No EU promotion until approved data flow and recovery comply. |
| 9 | Major | Sippy may repeat source transfers and does not refresh source mutations; Slurper can overwrite or skip large objects. Use immutable manifests, bounded batches and explicit failed/skipped reconciliation. |
| 10 | Major | Large-object throughput, browser cache churn, tiny-object ops and IA retrieval economics are unmeasured. Benchmark and observe the canary before broader rollout. |
| 11 | Major | Cloudflare compute is a new marketing/product request, while existing compute remains AWS. Define the actual offering before advertising availability. |

## 11. One-time egress estimate and recurring baseline

**Actual migration total: unknown pending live bytes, versions, classes and payer rates.** The source tree cannot supply a defensible aggregate estimate. Do not use catalog-declared external dataset sizes or multiply historical object counts by a guessed mean.

For an approved batch, calculate `transfer_cost = sum(billable_GB_by_region_and_tier * marginal_rate)`. Include only required source generations, then separately budget source retrieval/restore, requests, destination operations, verification re-reads, retries, NAT/proxy processing, and retained S3 storage during soak. Existing DR replicas must not be counted as a second logical dataset unless their unique generations are needed. Account for payer-level tier usage and unused allowances; do not assume a new free tier per bucket.

The user's $0.05-$0.09/GB range is a planning sensitivity, not a verified account rate. AWS's [S3 pricing page](https://aws.amazon.com/s3/pricing/) documents regional/tier-dependent transfer charges and a \$0.09/GB internet example. Obtain current applicable region/payer tiers and discounts before approving each batch.

| Illustrative billable GB, not measured Avala data | At \$0.05/GB | At \$0.09/GB |
| - | -: | -: |
| 1,000 | \$50 | \$90 |
| 10,000 | \$500 | \$900 |
| 100,000 | \$5,000 | \$9,000 |

This table assumes the stated rate throughout for sensitivity; actual tiered totals differ. Sippy is not an exactly-once bound, so its first-access rationale is not a hard cost ceiling. Migration and integration tests remain billable work requiring explicit approval.

The last three complete months for comparison are June, July and August 2026. All three S3 storage/request/egress amounts are currently unknown. R2 projection inputs are stored GB-month by class, A/B operations, IA retrieval GB, Workers/Queues/auth-serving costs and retained AWS costs. The reviewed Standard/IA unit prices are recorded in `notes.md` (in this folder; not in the site navigation). No monthly savings claim or cold-derived transition age is approved from the available evidence.
