Skip to content

Resend-style domain onboarding: per-customer SES identity (no 'via e2a' shim) #113

@jiashuoz

Description

@jiashuoz

Goal

Match Resend's outbound onboarding UX. After a user delegates their domain via DNS records, agent mail goes out as:

From: agent@inbox.mnexa.ai
mailed-by: send.inbox.mnexa.ai
signed-by: inbox.mnexa.ai

No via e2a display-name shim. Users never see AWS / SES — they paste ~5 DNS records into their DNS provider and watch chips turn green.

Today's state

The deployment-level shared-sender pattern works for deliverability but renders as:

From: agent@inbox.mnexa.ai via e2a <agent@send.e2a.dev>
mailed-by: mail.send.e2a.dev
signed-by: send.e2a.dev

Gmail surfaces the "via e2a" anywhere From-domain ≠ DKIM-domain. Mail delivers fine (SES's deployment DKIM aligns with the deployment From-domain), but the visible header doesn't match the customer's brand.

The per-domain DKIM keypair work in PR #112 (commit f022ae5) generates a keypair per domain and exposes the public key in DNS, but the actual deliverability win requires SES to be the signer, not us. See Resend-style comparison below.

The gap

Piece Status What's needed
SES per-customer identity Not built Call SES `CreateEmailIdentity` at domain registration → SES returns 3 DKIM tokens. Store identity ARN + tokens.
Custom MAIL FROM Not built Call `PutEmailIdentityMailFromAttributes` with `send.{customerDomain}`. SES returns 1 MX + 1 SPF TXT for the customer to publish.
Verification poller / webhook Not built SES emits SNS events when an identity flips to `SUCCESS`. Subscribe to SNS or poll `GetEmailIdentity` periodically — mark the domain "ready to send" only after SES confirms.
Sender rewrite Partial Drop the "via e2a" display-name shim. Set `From: bot@customerDomain` + envelope MAIL FROM = `bounce@send.customerDomain` when the domain is fully delegated. Fall back to today's shared-domain path when not.
Onboarding UI Partial Domain detail page renders the ~5 DNS records (3 DKIM CNAMEs + 1 MX + 1 SPF TXT + the existing inbound MX), each with a live "found / missing" chip.

The per-domain DKIM code from PR #112 becomes either deleted (SES handles signing once delegated) or kept as a self-hosted fallback. Recommend delete to keep one path.

Target user flow

What a user onboarding `inbox.mnexa.ai` would see:

  1. Add domain — enter `inbox.mnexa.ai`
  2. One screen lists 5 DNS records to paste, each labeled with purpose:
    • DKIM 1 of 3 (CNAME)
    • DKIM 2 of 3 (CNAME)
    • DKIM 3 of 3 (CNAME)
    • Outbound mail return path (MX on `send.inbox.mnexa.ai`)
    • Outbound mail SPF (TXT on `send.inbox.mnexa.ai`)
    • Plus the existing inbound MX record
  3. Real-time status — e2a polls SES + DNS, each record's chip turns green as DNS propagates.
  4. All green → "Ready to send" chip appears. Agent mail goes out as `From: agent@inbox.mnexa.ai`, signed by `inbox.mnexa.ai`, no shim.

What they don't see: AWS, SES, the identity ARN, the DKIM tokens themselves. The CNAME targets are `*.dkim.amazonses.com` and similar — opaque infrastructure details from their perspective.

Recommended PR split

  1. SES management plumbing (backend-only, no UX change). New `internal/sesidentity` package wrapping the SES v2 API. DB migration for `domains.ses_identity_arn` + `ses_dkim_tokens` + `mail_from_status`. Background verification poller. — Load-bearing piece, build first.
  2. Onboarding UI revamp. Domain detail page shows the 5-record DNS checklist with live status. Replaces today's single-TXT verification step.
  3. Sender cutover. Change `From` + envelope MAIL FROM when domain is fully delegated. Keep shared-domain agents on today's path. Delete the self-signing code in `outbound.Sender`.

Footguns

  • SES sandbox. New SES accounts start in sandbox mode (can only send to pre-verified addresses). The current production deployment is presumably already out (since mail is delivering), but worth confirming. Re-deploying into a fresh AWS account = one-time AWS support ticket + ~24h wait. Doesn't affect users, only deployment.
  • Region scoping. SES identities are region-scoped. Fine for single-region deploys; multi-region needs identity replication.
  • Bounce / complaint handling. SES emits SNS notifications for bounces and complaints. Need to subscribe + handle (suppression list) — currently we let SES handle it pool-wide, but per-customer reputation will require per-customer handling.
  • IAM. Deployment role needs `ses:CreateEmailIdentity`, `ses:GetEmailIdentity`, `ses:PutEmailIdentityMailFromAttributes`, `ses:DeleteEmailIdentity`.

Reference: how Resend achieves this

When a user onboards `mnexa.ai` with Resend, they're given CNAMEs that delegate DKIM signing authority to Resend's keys (`*.dkim.resend.com` style). Resend signs with `d=mnexa.ai` because their DNS lookup for `resend._domainkey.mnexa.ai` resolves through the CNAME chain to their key material. SPF aligns under `send.mnexa.ai` (a subdomain the customer CNAMEs to Resend's infra), which is why Gmail shows `mailed-by: send.mnexa.ai`. Same architectural pattern works for us via SES.

Effort estimate

~5–8 days of focused work for a usable v1, broken across the three PRs above. PR #1 is the meaty chunk (SES SDK integration + state machine for verification); #2 is mostly UI work; #3 is mechanical.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions