Deployment

Deploy mail-catcher to dev and production with DNS and certificate setup.

mail-catcher deploys to AWS using SST v4 (built on Pulumi). This guide covers DNS setup, ACM certificates, and production deployments.

Prerequisites

  • Environment variables configured (see Configuration)
  • AWS credentials with permissions for Lambda, S3, DynamoDB, SES, CloudFormation, and IAM

If you're using a custom API_DOMAIN without Route 53, you must provision an ACM certificate before deploying. See Step 2.

Step 1: Deploy

bun run deploy:dev       # dev stage
bun run deploy:prod      # production stage

Each stage creates its own isolated set of resources:

  • S3 bucket for raw .eml files (8-day lifecycle)
  • DynamoDB tables for email metadata (7-day TTL) and API keys
  • API Lambda (Hono HTTP handler)
  • Ingest Lambda (S3 event handler)
  • SES domain identity and receipt rules
  • CloudFront distribution (only when API_DOMAIN is set)
  • DNS records in Route 53 (only when HOSTED_ZONE_ID is set)

Step 2: Provision ACM certificate (external DNS only)

Skip this if you're using Route 53 (HOSTED_ZONE_ID is set).

If you set API_DOMAIN without HOSTED_ZONE_ID, a validated ACM certificate must exist before deployment.

Request the certificate

The certificate must be in the same region as AWS_REGION:

aws acm request-certificate \
  --domain-name api.inbox.yourdomain.com \
  --validation-method DNS \
  --region $AWS_REGION

Add the validation CNAME

aws acm describe-certificate \
  --certificate-arn <arn-from-above> \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord" \
  --region $AWS_REGION

Add the returned CNAME record to your DNS provider.

Wait for issuance

aws acm describe-certificate \
  --certificate-arn <arn-from-above> \
  --query "Certificate.Status" \
  --region $AWS_REGION

Do not deploy until the status is ISSUED. A pending certificate causes the deploy to fail with reading ACM Certificates: empty result.

Step 3: Configure DNS records for SES

For SES to receive emails on your domain, two DNS records are required:

MX record (email routing)

This tells mail servers to route emails for your domain to AWS SES inbound:

TypeNamePriorityValue
MXreceive.yourdomain.com10inbound-smtp.<region>.amazonaws.com

Replace <region> with your AWS_REGION. For example:

  • us-east-1inbound-smtp.us-east-1.amazonaws.com
  • us-west-2inbound-smtp.us-west-2.amazonaws.com
  • eu-west-1inbound-smtp.eu-west-1.amazonaws.com

TXT record (domain verification)

SES requires a TXT record to prove you own the domain. The verification token is generated during deployment and included in the deploy output:

TypeNameValue
TXT_amazonses.receive.yourdomain.com<verification-token from deploy output>

CNAME record (custom API domain only)

If you set API_DOMAIN, a CloudFront distribution is created. You need a CNAME pointing your custom domain to it:

TypeNameValue
CNAMEapi.inbox.yourdomain.com<cloudfront-domain from deploy output>

Automatic vs manual DNS setup

  • Route 53 (HOSTED_ZONE_ID set): all three records are created automatically. No manual steps needed.
  • External DNS (HOSTED_ZONE_ID omitted): the deploy writes a .sst/dns-records.zone BIND zone file with the exact records and values you need to add.

Adding records manually

Cloudflare: Go to DNS → Records → Import and Export → Upload .sst/dns-records.zone. After importing, set the API CNAME record to DNS only (grey cloud). Proxying through Cloudflare causes Error 1016 because CloudFront must terminate TLS directly.

Other providers: Open .sst/dns-records.zone and add the records manually.

Verify DNS propagation

After adding the records, verify they're live:

dig MX receive.yourdomain.com
dig TXT _amazonses.receive.yourdomain.com

Expected output for MX:

receive.yourdomain.com. 300 IN MX 10 inbound-smtp.us-east-1.amazonaws.com.

Then confirm your domain shows as Verified in the SES Console.

DNS propagation typically takes 5–15 minutes but can take up to 48 hours depending on your provider. SES will not receive emails until both records are live and the domain is verified.

Step 4: Verify the deployment

curl https://<your-api-url>/health

The apiUrl is printed in the deploy output and saved to .sst/outputs.json.

Teardown

bun run remove:dev       # deletes all dev stage resources
  • dev stage: all resources are deleted on removal
  • prod stage: resources are retained on removal (safety measure). To fully delete, remove resources manually or change the removal policy in sst.config.ts.

Search Documentation

Search through the docs