Skip to content

Custom Domain Setup

Copilot edited this page Jul 11, 2026 · 1 revision

Custom Domain Setup

By default the stack is only reachable at its auto-generated execute-api.<region>.amazonaws.com URL. This page covers serving both /send and /admin from your own hostname instead (e.g. app.nmailx.com)

  • this is a different feature from the project's own marketing site, which lives at www.nmailx.com via GitHub Pages (see GitHub Pages and DNS). The custom domain covered here is for your deployed API, not the project's landing page.

Why a separate bootstrap stack for the certificate

bootstrap/custom-domain.yaml is a deliberately isolated, one-time stack, not part of the main template.yaml. API Gateway custom domains are an already-approved service for this project, but ACM certificate issuance is kept separate because DNS for this project lives in Cloudflare, not Route53, and CloudFormation can't automate DNS validation against a non-Route53 provider. Splitting it out means the main stack's day-to-day deploys never block on an external DNS step.

Step 1: issue the ACM certificate

make bootstrap-domain-cert DOMAIN_NAME=app.nmailx.com   # blocks until DNS-validated

This call blocks - ACM certificates stay CREATE_IN_PROGRESS until DNS validation succeeds. While it's running, open a second terminal and run:

make bootstrap-domain-cert-validation-record DOMAIN_NAME=app.nmailx.com

...to print the DNS validation CNAME (name + value). Add it in Cloudflare DNS as DNS-only (not proxied) - a proxied record breaks ACM's DNS-based validation, since Cloudflare's proxy intercepts the request before it reaches the ACM validation checker. Once it propagates, ACM validates automatically and the first command completes, printing the stack's CertificateArn output. You can re-run make bootstrap-domain-cert-validation-record any time afterward to fetch the same value again.

Region requirement: bootstrap/custom-domain.yaml must be deployed in the same AWS region as the main stack (us-east-1 by default) - regional API Gateway custom domains require the certificate to live in that same region, unlike CloudFront/edge-optimized domains, which always use us-east-1 regardless of where the rest of the stack lives.

Step 2: deploy the main stack with the custom domain parameters

JWT_SIGNING_SECRET=... TURNSTILE_SECRET_KEY=... \
CUSTOM_DOMAIN_NAME=app.nmailx.com \
CUSTOM_DOMAIN_CERTIFICATE_ARN=<arn from step 1> \
make deploy

make outputs will then show a CustomDomainTarget value (the API Gateway-managed regional domain name). Add one more CNAME in Cloudflare DNS pointing app.nmailx.com at that target - again, DNS-only, not proxied. Once it propagates, both https://app.nmailx.com/send and https://app.nmailx.com/admin work alongside the original execute-api URL. Both remain live - the custom domain is an additional mapping, not a replacement for the auto-generated one.

CustomDomainName and CustomDomainCertificateArn both default to empty, which leaves default behavior completely unchanged: the custom domain resources in template.yaml (an AWS::ApiGatewayV2::DomainName and an AWS::ApiGatewayV2::ApiMapping, gated behind a HasCustomDomain CloudFormation condition) are only created when both parameters are set.

Tearing a custom domain back down

See Teardown for the full sequence - in short, make delete-domain-cert removes the bootstrap certificate stack, and you should also remove the Cloudflare CNAMEs you added (the app.nmailx.com target record and the ACM validation record) once the domain is no longer in use.

Clone this wiki locally