Skip to main content

AWS Service Baseline

Status

Locked first-pass direction for #57.

VRDex uses AWS narrowly for early supporting infrastructure. The current baseline is intentionally small: a transactional email sender identity, private profile asset storage, and no broad AWS application platform before the product needs it. Authentication email is Clerk's, not SES's.

Scope

The first AWS baseline covers:

  • Amazon SES, retired for authentication now that Clerk sends its own verification email; see ses-auth-email.md
  • Route 53 DNS records for the SES sender domain
  • IAM credentials scoped to SES sending for Convex
  • a private S3 asset bucket for owner-authored profile assets, tracked by #115
  • Terraform state in S3 for checked-in infrastructure stacks
  • a validation-only hosted restream worker benchmark foundation for ECR, ECS/Fargate, task roles, logs, metrics, secret references, limits, and kill-switch concepts

Non-goals for this baseline:

  • full production AWS landing zone
  • multi-account AWS architecture
  • CloudFront image CDN and transformation pipeline
  • broad object lifecycle, malware scanning, moderation review, or asset-processing automation
  • moving the application runtime from Convex/Vercel to AWS
  • promising hosted restreaming, GPU capacity, AWS-owned CDN output, or 1080p60 pricing before local and hosted benchmark evidence exists

Email Delivery

Locked decision: use Amazon SES for transactional email. Auth verification is no longer part of this — Clerk sends its own verification and password email, so the SES stack is retained for its domain identity and DKIM records rather than rewired into a sign-in flow. Nothing in the codebase sends through SES today; see ses-auth-email.md.

Current hosted baseline:

  • AWS account: BASIC BIT hosted production account; provider account IDs stay in provider configuration, Terraform state, or operator records rather than public docs
  • SES region: us-east-1
  • sending domain identity: vrdex.net
  • sender address: no-reply@vrdex.net
  • Route 53 hosted zone: vrdex.net hosted zone; provider-generated hosted zone IDs stay in provider configuration, Terraform state, or operator records rather than public docs
  • Terraform stack: infra/terraform/ses
  • Terraform state key: ses/terraform.tfstate

Verified state as of this baseline pass:

  • SES identity verification: Success
  • Easy DKIM: enabled
  • DKIM verification: Success
  • Terraform plan against the hosted stack: no changes
  • SES send quota: Max24HourSend=50000, MaxSendRate=14, SentLast24Hours=0

A Convex deployment that sends email through SES must set the following. None do today — see the retirement note above:

  • AWS_SES_REGION
  • AWS_SES_FROM_EMAIL
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • VRDEX_APP_NAME optional display name for email copy

The access key is generated by Terraform only when create_iam_access_key=true. Store the secret in Convex or the relevant provider secret store, never in git.

Asset Storage

Locked first-pass direction: use a private Amazon S3 bucket for profile media-kit assets.

The first asset-storage implementation uses:

  • S3 Block Public Access enabled at the bucket level
  • server-side encryption; SSE-S3 is acceptable for the first slice because S3 encrypts new object uploads by default
  • authenticated Convex upload intents plus token-gated Next.js upload/import routes for controlled browser uploads
  • import-by-URL guarded to public HTTPS destinations with bounded response sizes before any object is written
  • direct browser POSTs into a quarantine prefix using short-lived presigned policies bound to one object key, exact byte size, and declared content type
  • content-derived image validation, bounded decoded dimensions, exact private source preservation, original-format metadata-sanitized downloads, optimized WebP displays, and a restricted SVG profile before publication
  • app-generated reads through /api/v0/profiles/:slug/assets/:assetId/file and /api/v0/profiles/:slug/logos.zip, instead of public bucket objects
  • deterministic object prefixes keyed by asset upload date and upload intent token
  • metadata sufficient to connect uploaded objects to profile records, uploader, upload time, and moderation/review state

Do not make profile asset buckets public. Public profile pages should render through a controlled URL path that can enforce profile visibility, moderation suppression, replacement, and deletion behavior.

Operational probe:

  • GET /api/v0/profile-assets/upload-intents/probe returns 501 when runtime storage environment variables are missing.
  • The probe is an anonymous public-read endpoint, but it still rejects bearer tokens in query parameters and applies the standard public API rate limit. Use it for bounded deployment checks, not a tight polling loop.
  • A configured environment performs a lightweight private S3 HeadObject check against a sentinel key and returns 200 when storage auth is reachable.
  • The runtime role includes narrow s3:ListBucket permission for only that sentinel key prefix so a missing sentinel is reported as an object-level miss, not as an authorization failure.
  • The probe returns only coarse health state and does not expose bucket names, role ARNs, or object keys.

Terraform/runtime baseline:

  • Terraform stack: infra/terraform/profile-assets
  • Terraform state key: profile-assets/terraform.tfstate
  • Hosted bucket name default: vrdex-profile-assets-${account_id}
  • Hosted runtime auth: Vercel OIDC, scoped to the vr-dex-web project and the allowed production/staging environments

The first owner-facing gallery keeps at most 12 active public assets per profile. Deletion is recoverable metadata state; it does not synchronously delete the private object. Before the launch flag is enabled, add a 24-object / 288 MB retained-storage cap per profile, reconcile expired or orphaned objects within 24 hours, and remove soft-deleted objects after a 30-day recovery window.

Abandoned direct-upload objects under profile-assets/quarantine/ expire after two days through the checked-in S3 lifecycle rule. This bounds uploads that never reach completion; it does not replace the application reconciliation job for post-validation variant writes or the 30-day hard-delete process for recoverable assets.

Deferred follow-on work:

  • moderation or malware scanning
  • CloudFront, responsive variants, or a dedicated image CDN
  • post-write orphan reconciliation and recoverable-asset hard deletion

Runtime environment/config names:

  • VRDEX_PROFILE_MEDIA_KIT_ENABLED=true in both the web and Convex runtimes only after the hosted upload/read/download smoke test and retained-storage launch gates pass; absence keeps owner gallery entry points and mutations disabled
  • VRDEX_PROFILE_MEDIA_DIRECT_UPLOAD_ENABLED=true in both Vercel and Convex only after the S3 CORS/lifecycle Terraform change and synthetic staging source/display/download smoke pass
  • VRDEX_PROFILE_MEDIA_ACCESSIBILITY_GENERATION_ENABLED=true in both Vercel and Convex only after the owner/rate/timeout staging smoke; OPENAI_API_KEY and optional VRDEX_PROFILE_MEDIA_ACCESSIBILITY_MODEL belong in Vercel only
  • VRDEX_PROFILE_ASSET_BUCKET or fallback VRDEX_ASSET_BUCKET
  • VRDEX_PROFILE_ASSET_REGION, fallback AWS_REGION, or fallback AWS_DEFAULT_REGION
  • VRDEX_PROFILE_ASSET_ROLE_ARN for hosted Vercel OIDC role-based auth
  • AWS runtime credentials through the hosting provider, role-based auth, or a narrow access key only if the runtime cannot use role-based auth
  • VRDEX_ASSET_PUBLIC_BASE_URL only after a controlled public delivery layer exists

Terraform State

Terraform stacks use the S3 backend bucket vrdex-terraform-state in us-east-1 with stack-specific state keys and S3 native locking.

Current stacks:

  • infra/terraform/state-mgmt: local-state bootstrap stack for the shared S3 Terraform state bucket
  • infra/terraform/ses: SES domain identity, DKIM, MAIL FROM, Route 53 records, and optional IAM sender key
  • infra/terraform/posthog: hosted PostHog project metadata
  • infra/terraform/vercel: hosted Vercel PostHog client environment variables
  • infra/terraform/profile-assets: private S3 asset bucket, Vercel OIDC IAM role, and hosted profile asset env vars
  • infra/terraform/docs-site: hosted docs Vercel project/domain and Route 53 DNS
  • infra/terraform/restream-worker: validation-only hosted worker benchmark foundation; CI validates it but does not plan or apply it
  • infra/terraform/vrclinking-adapter: VRCLinking proof adapter Lambda, Function URL, execution role, and log group. Deployed and live; CI validates but does not plan it, because planning needs the built artifact and the shared-secret ARN. Applied manually with -var-file=environments/production.tfvars, which is mandatory — see the stack README

Keep stack state, plans, local provider caches, and terraform.tfvars uncommitted.

Hosted Restream Worker Benchmark

Current recommendation: keep hosted restreaming behind the 1080p60 evidence gate.

The first hosted worker foundation lives in infra/terraform/restream-worker and workers/restream. It defines an ECR repository, ECS cluster, Fargate task definition, CloudWatch log group, task roles, optional secret-reference injection, max worker/runtime guardrails, and an SSM kill-switch parameter that defaults disabled.

This stack is intentionally validation-only in CI. Do not apply it, publish images, run ECS tasks, or mutate AWS resources until local media-pipeline evidence exists and a human approves an AWS benchmark window.

Hosted worker secret values must stay in Secrets Manager or an equivalent encrypted provider secret store. Convex event records and Terraform should carry scoped references only, not stream keys, ingest URLs, or output credentials.

Fargate remains the first benchmark path. ECS on EC2 with GPU/NVENC is a measured fallback only if CPU-only Fargate misses real-time 1080p60, transition quality, bitrate stability, or cost headroom.

Self-Hosting Boundary

Self-hosted AWS usage is limited to the values this repo documents by name: SES sender identity, DNS zone, Terraform state location, and the S3 asset bucket tracked by #115.

This baseline does not claim complete self-hosting support. Alternative S3-compatible stores are out of scope until a follow-up issue or ADR covers provider portability.