SES Auth Email
Current Recommendation
Retired for authentication, live for the support digest. Clerk sends its own
verification and password email, so SES is no longer part of sign-in. See
auth-sessions.md.
This section previously asked whether any other feature had adopted SES before
anyone removed the infrastructure. One has: the hourly
internal.supportRequestDigest.sendSupportDigest cron mails new /support
requests through this identity. Deleting the domain identity or the IAM key now
breaks it, and the breakage is silent, because requests keep landing in
supportRequests with nobody told about them.
Do not wire it back into an auth flow.
The Terraform stack at infra/terraform/ses provisions the SES domain identity, DKIM, custom MAIL FROM records, and an optional least-privilege IAM access key for Convex.
Current hosted baseline:
- domain identity:
vrdex.net - sender:
no-reply@vrdex.net - region:
us-east-1 - 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 state key:
ses/terraform.tfstate
As of the AWS baseline pass, SES identity verification and DKIM verification are both Success, and Terraform reports no drift for the hosted SES stack.
Convex Environment Variables
Set these in each Convex deployment that sends email:
AWS_SES_REGIONAWS_SES_FROM_EMAILAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYVRDEX_APP_NAMEoptional display name for email copyVRDEX_SUPPORT_DIGEST_TO: where the hourly/supportdigest is delivered. Without it the cron returnsconfigured: falseand sends nothing, which is the correct state for a deployment that has no operator mailbox. Requests keep their unsetnotifiedAtmeanwhile, so setting this later delivers the backlog rather than starting from whatever arrives next.
configured: true is not proof of delivery. It means the three variables are
present, not that SES accepted the message, so confirm a real digest arrived
after setting the recipient for the first time.
Support Digest Rollout Order
/support accepts requests whether or not anyone is listening, and tells every
requester it succeeded. That is correct behaviour, since a request kept with an
unset notifiedAt is delivered whenever the recipient is configured, but it
means the window between shipping the route and configuring delivery is one
where people are told they have been heard and have not been. Keep it short and
deliberate:
- Set
VRDEX_SUPPORT_DIGEST_TOon the deployment. - Run
internal.supportRequestDigest.sendSupportDigestby hand from the Convex dashboard and confirm the mail actually arrives. SPF and DMARC failures are silent from Convex's side, andconfigured: truedoes not see them. - Only then announce the route.
Until step 1 is done, the hourly cron logs a warning naming the number of requests waiting, so an operator wondering why nobody has answered has something to find. It is deliberately quiet when nothing is waiting, which is the ordinary state of a deployment with no operator mailbox.
Adapter Environment Variables
Discord community Administrator verification:
DISCORD_BOT_TOKEN: Discord bot token for reading guild, member, and role stateDISCORD_API_BASE_URL: optional override, defaults tohttps://discord.com/api/v10
The bot must be present in claimed guilds and able to read members and roles.
VRChat and VRCLinking proof-code verification:
VRCHAT_PROOF_ADAPTER_URL: POST endpoint for VRChat user/group proof checksVRCLINKING_PROOF_ADAPTER_URL: POST endpoint for VRCLinking proof checksVRCHAT_PROOF_ADAPTER_BEARER_TOKEN: required whenever either adapter URL is set, and must match the value the adapter itself requires. Convex refuses to call an adapter without it rather than sending an unauthenticated request that the adapter would answer with a 401 — which the claim path reads as the non-terminalunavailable, so the claim would stall with the misconfiguration reported nowhere.VRCLINKING_ADAPTER_CAPABILITY_KEY: required wheneverVRCLINKING_PROOF_ADAPTER_URLis set. Signs the per-delegation capability the adapter verifies asVRDEX_VRCLINKING_CAPABILITY_KEY. Keep it a different value from the bearer token: the bearer token authenticates the channel, and this authorizes the individual guild, so a leak of the first must not confer the second.
Adapter URLs must use https, or http only on loopback for a local stub.
Every request carries the bearer token.
The two adapters receive different payloads, and an implementation written against the wrong one waits for fields it never gets:
VRCHAT_PROOF_ADAPTER_URLreceivestargetType(vrchat_userorvrchat_group),targetExternalId,proofCode, and safe profile context. It answers whether the code is present on that target.VRCLINKING_PROOF_ADAPTER_URLreceivestargetType: "vrclinking",targetExternalId,discordUserId, and adelegationsarray of{ guildId, secretRef, expiresAt, capability }. It receives no proof code and no profile context — it answers from a delegated key, so neither is of any use to it. Seedocs/backend/vrclinking-api.mdfor the delegation contract and what a positive result must carry back.
Both return JSON with verified, evidenceSource, and evidenceSummary.
Sandbox Note
SES domain verification and DKIM do not automatically move an AWS account out of SES sandbox mode. Request SES production access in AWS before relying on real user emails outside verified recipient addresses.
The hosted AWS account currently has a production-shaped SES quota. Keep this documented status current if the SES region, sender domain, or account changes.
Relationship To AWS Baseline
The broader AWS baseline, including private S3 profile assets tracked by #115, lives in docs/deployment/aws-baseline.md.