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
1080p60pricing 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 was never rewired into a sign-in flow.
It is not idle, though. The hourly support digest,
internal.supportRequestDigest.sendSupportDigest, mails new /support requests
through this identity, so the credentials below are live rather than retained
for their DNS records. Removing them stops disputes, transfers, recoveries, and
opt-outs from reaching anyone, and stops them quietly. 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.nethosted 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. Both hosted deployments set the four AWS values; the digest recipient is still outstanding on both, so support mail is not live yet and requests accumulate unannounced until it is:
AWS_SES_REGIONAWS_SES_FROM_EMAILAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYVRDEX_APP_NAMEoptional display name for email copyVRDEX_SUPPORT_DIGEST_TOthe mailbox the support digest is delivered to. Without it the digest is switched off and requests accumulate with nobody notified
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/fileand/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/probereturns501when 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
HeadObjectcheck against a sentinel key and returns200when storage auth is reachable. - Production allows
s3:ListBucketfor only the sentinel key prefix. Staging allows it on its separate private bucket so missing media keys return 404 during hosted cleanup verification instead of an ambiguous 403. - 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 staging bucket name default:
vrdex-profile-assets-staging-${account_id} - Hosted runtime auth: separate Vercel OIDC roles scoped to the
vr-dex-webproject's production and staging environments respectively
The existing production bucket and runtime role remain in place. The staging
custom environment variables must point to the staging bucket and staging role
before hosted upload testing. Apply the updated state-mgmt CI permissions,
review the profile-assets plan, then apply and redeploy staging promptly.
Existing staging deployments can briefly lose S3 access after production role
trust narrows and before the new staging deployment is serving. Verify the
staging storage probe and exact staging bucket binding before enabling staged
upload or contribution flags.
Local media upload verification uses a separate non-production bucket defined by
infra/terraform/profile-assets-proof.
Its state key is profile-assets-proof/terraform.tfstate and its account-derived
bucket name is vrdex-profile-assets-proof-${account_id} in us-east-1.
The proof bucket blocks public access, enforces bucket ownership, uses SSE-S3,
and denies non-TLS requests. Only profile-assets/proof/local-upload/ expires
after seven days. It has no Vercel configuration or production bucket access.
Terraform CI validates this stack only. An operator reviews its local plan and
applies it with approved credentials before running the
local upload proof.
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=truein 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 disabledVRDEX_PROFILE_MEDIA_DIRECT_UPLOAD_ENABLED=truein both Vercel and Convex only after the S3 CORS/lifecycle Terraform change and synthetic staging source/display/download smoke passVRDEX_PROFILE_MEDIA_SUBMISSIONS_ENABLED=truein both Vercel and Convex only after reviewer access, private candidate review, retention cleanup, and contributor-to-approval staging smokes pass; disabling it stops new submissions and decisions without hiding already-approved assetsVRDEX_PROFILE_MEDIA_ACCESSIBILITY_GENERATION_ENABLED=truein both Vercel and Convex only after the owner/rate/timeout staging smoke;OPENAI_API_KEYand optionalVRDEX_PROFILE_MEDIA_ACCESSIBILITY_MODELbelong in Vercel onlyVRDEX_PROFILE_ASSET_BUCKETor fallbackVRDEX_ASSET_BUCKETVRDEX_PROFILE_ASSET_REGION, fallbackAWS_REGION, or fallbackAWS_DEFAULT_REGIONVRDEX_PROFILE_ASSET_ROLE_ARNfor 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_URLonly 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 bucketinfra/terraform/ses: SES domain identity, DKIM, MAIL FROM, Route 53 records, and optional IAM sender keyinfra/terraform/posthog: hosted PostHog project metadatainfra/terraform/vercel: hosted Vercel PostHog client environment variablesinfra/terraform/profile-assets: private S3 asset bucket, Vercel OIDC IAM role, and hosted profile asset env varsinfra/terraform/profile-assets-proof: dedicated non-production S3 upload proof bucket; CI validation only, operator plan/applyinfra/terraform/docs-site: hosted docs Vercel project/domain and Route 53 DNSinfra/terraform/restream-worker: validation-only hosted worker benchmark foundation; CI validates it but does not plan or apply itinfra/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.