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_DOMAINwithout 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 stageEach stage creates its own isolated set of resources:
- S3 bucket for raw
.emlfiles (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_DOMAINis set) - DNS records in Route 53 (only when
HOSTED_ZONE_IDis 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_REGIONAdd the validation CNAME
aws acm describe-certificate \
--certificate-arn <arn-from-above> \
--query "Certificate.DomainValidationOptions[0].ResourceRecord" \
--region $AWS_REGIONAdd 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_REGIONDo 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:
| Type | Name | Priority | Value |
|---|---|---|---|
MX | receive.yourdomain.com | 10 | inbound-smtp.<region>.amazonaws.com |
Replace <region> with your AWS_REGION. For example:
us-east-1→inbound-smtp.us-east-1.amazonaws.comus-west-2→inbound-smtp.us-west-2.amazonaws.comeu-west-1→inbound-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:
| Type | Name | Value |
|---|---|---|
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:
| Type | Name | Value |
|---|---|---|
CNAME | api.inbox.yourdomain.com | <cloudfront-domain from deploy output> |
Automatic vs manual DNS setup
- Route 53 (
HOSTED_ZONE_IDset): all three records are created automatically. No manual steps needed. - External DNS (
HOSTED_ZONE_IDomitted): the deploy writes a.sst/dns-records.zoneBIND 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.comExpected 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>/healthThe 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.