| 1 | --- |
| 2 | title: Custom domains with Route53 + ACM |
| 3 | description: Serve your site or API from your own domain — DNS-validated ACM certificates, Route 53 alias records, and the domain prop on Website resources. |
| 4 | sidebar: |
| 5 | order: 6 |
| 6 | --- |
| 7 | |
| 8 | A custom domain on AWS is three moving parts: a TLS certificate (ACM), DNS |
| 9 | records (Route 53), and wiring both into whatever serves traffic (CloudFront |
| 10 | or API Gateway). For websites, Alchemy collapses all three into a single |
| 11 | `domain` prop; for APIs and anything bespoke, the same primitives — |
| 12 | `AWS.ACM.Certificate` and `AWS.Route53.Record` — are available directly. |
| 13 | |
| 14 | ## The one-prop path: `domain` on Website resources |
| 15 | |
| 16 | `AWS.Website.Router`, `AWS.Website.SsrSite`, and a standalone |
| 17 | `AWS.Website.StaticSite` all accept the same `domain` shape: |
| 18 | |
| 19 | ```typescript |
| 20 | const router = yield* AWS.Website.Router("WebsiteRouter", { |
| 21 | domain: { |
| 22 | name: "app.example.com", |
| 23 | hostedZoneId: "Z1234567890", |
| 24 | aliases: ["www.app.example.com"], |
| 25 | }, |
| 26 | routes: { |
| 27 | "/*": site.routeTarget, |
| 28 | }, |
| 29 | }); |
| 30 | ``` |
| 31 | |
| 32 | With `hostedZoneId` set, Alchemy requests a DNS-validated ACM certificate |
| 33 | covering `name` plus every alias, upserts the validation records into your |
| 34 | zone, waits for issuance, attaches the certificate to the CloudFront |
| 35 | distribution, and creates an `A` alias record for each hostname. The |
| 36 | resource's `url` output becomes `https://app.example.com`. When a site is |
| 37 | attached to a `Router`, put `domain` on the Router — passing it to the |
| 38 | attached `StaticSite` as well is an error. |
| 39 | |
| 40 | ## Bring your own certificate |
| 41 | |
| 42 | If you already have an ACM certificate, pass its ARN as `cert` and Alchemy |
| 43 | skips certificate creation: |
| 44 | |
| 45 | ```typescript |
| 46 | domain: { |
| 47 | name: "app.example.com", |
| 48 | hostedZoneId: "Z1234567890", |
| 49 | cert: existingCertificateArn, |
| 50 | }, |
| 51 | ``` |
| 52 | |
| 53 | DNS records are still managed for you — only the certificate step is |
| 54 | replaced. The certificate must cover the domain name and every alias you |
| 55 | list. |
| 56 | |
| 57 | ## Manage DNS elsewhere |
| 58 | |
| 59 | Set `dns: false` to skip Route 53 entirely: |
| 60 | |
| 61 | ```typescript |
| 62 | domain: { |
| 63 | name: "app.example.com", |
| 64 | cert: existingCertificateArn, |
| 65 | dns: false, |
| 66 | }, |
| 67 | ``` |
| 68 | |
| 69 | No records are created, so you point your DNS provider at the distribution's |
| 70 | domain name yourself. `dns: false` requires `cert` — without Route 53 access |
| 71 | Alchemy can't complete DNS validation for a new certificate, so you must |
| 72 | bring one. |
| 73 | |
| 74 | ## Create or reference a hosted zone |
| 75 | |
| 76 | The `domain` prop and the primitives below both key off a hosted zone ID. If |
| 77 | the zone lives outside your stack, pass its ID as a plain string; to manage |
| 78 | it in the stack, use `AWS.Route53.HostedZone`: |
| 79 | |
| 80 | ```typescript |
| 81 | const zone = yield* AWS.Route53.HostedZone("Zone", { |
| 82 | name: "example.com", |
| 83 | }); |
| 84 | ``` |
| 85 | |
| 86 | `zone.id` is the hosted zone ID (without the `/hostedzone/` prefix) and |
| 87 | `zone.nameServers` lists the authoritative name servers — set these as `NS` |
| 88 | records at your registrar (or parent zone) to delegate the domain. Set |
| 89 | `forceDestroy: true` if you want `destroy` to delete any remaining records |
| 90 | before deleting the zone. |
| 91 | |
| 92 | ## Request a DNS-validated certificate |
| 93 | |
| 94 | `AWS.ACM.Certificate` requests a certificate and, when you give it a |
| 95 | `hostedZoneId`, handles validation end to end: |
| 96 | |
| 97 | ```typescript |
| 98 | const cert = yield* AWS.ACM.Certificate("WebsiteCertificate", { |
| 99 | domainName: "app.example.com", |
| 100 | subjectAlternativeNames: ["www.app.example.com"], |
| 101 | hostedZoneId: zone.id, |
| 102 | }); |
| 103 | ``` |
| 104 | |
| 105 | The provider upserts the DNS validation records ACM asks for into the zone, |
| 106 | waits for Route 53 to propagate them, and blocks until the certificate |
| 107 | reaches `ISSUED` — so `cert.certificateArn` is ready to attach the moment |
| 108 | the effect resolves. Certificates are requested in `us-east-1`, the region |
| 109 | CloudFront requires for viewer certificates. ACM certificates are immutable: |
| 110 | changing the domain name, SANs, or validation settings replaces the |
| 111 | certificate with a new one. |
| 112 | |
| 113 | ## Alias a record to CloudFront |
| 114 | |
| 115 | `AWS.Route53.Record` manages a single record set; an `A` record with |
| 116 | `aliasTarget` points a hostname at a CloudFront distribution: |
| 117 | |
| 118 | ```typescript |
| 119 | yield* AWS.Route53.Record("WebsiteAlias", { |
| 120 | hostedZoneId: zone.id, |
| 121 | name: "app.example.com", |
| 122 | type: "A", |
| 123 | aliasTarget: { |
| 124 | hostedZoneId: distribution.hostedZoneId, |
| 125 | dnsName: distribution.domainName, |
| 126 | }, |
| 127 | }); |
| 128 | ``` |
| 129 | |
| 130 | Note that `aliasTarget.hostedZoneId` is the *distribution's* hosted zone ID |
| 131 | (an attribute CloudFront exposes), not your own zone's. Records are written |
| 132 | with `UPSERT` — create and update are the same operation — and the provider |
| 133 | waits for the Route 53 change to reach `INSYNC` before returning, so the |
| 134 | record is live when the deploy finishes. |
| 135 | |
| 136 | ## Custom domain for API Gateway |
| 137 | |
| 138 | For a REST API, create an `AWS.ApiGateway.DomainName` with a certificate and |
| 139 | map your API's stage onto it with a `BasePathMapping`: |
| 140 | |
| 141 | ```typescript |
| 142 | const domain = yield* AWS.ApiGateway.DomainName("ApiDomain", { |
| 143 | domainName: "api.example.com", |
| 144 | regionalCertificateArn: cert.certificateArn, |
| 145 | endpointConfiguration: { types: ["REGIONAL"] }, |
| 146 | securityPolicy: "TLS_1_2", |
| 147 | }); |
| 148 | |
| 149 | yield* AWS.ApiGateway.BasePathMapping("Root", { |
| 150 | domainName: domain.domainName, |
| 151 | restApiId: api.restApiId, |
| 152 | stage: "prod", |
| 153 | }); |
| 154 | ``` |
| 155 | |
| 156 | One region caveat: a `REGIONAL` domain needs its certificate in the API's own |
| 157 | region, while `AWS.ACM.Certificate` always issues in `us-east-1`. Deploying |
| 158 | to `us-east-1` works as written; in any other region, pass the ARN of a |
| 159 | certificate that already exists there as `regionalCertificateArn`. |
| 160 | |
| 161 | ## Point DNS at the API |
| 162 | |
| 163 | The `DomainName` resource exposes the regional endpoint to alias to: |
| 164 | |
| 165 | ```typescript |
| 166 | yield* AWS.Route53.Record("ApiAlias", { |
| 167 | hostedZoneId: zone.id, |
| 168 | name: "api.example.com", |
| 169 | type: "A", |
| 170 | aliasTarget: { |
| 171 | hostedZoneId: domain.regionalHostedZoneId.as<string>(), |
| 172 | dnsName: domain.regionalDomainName.as<string>(), |
| 173 | }, |
| 174 | }); |
| 175 | ``` |
| 176 | |
| 177 | `regionalDomainName` / `regionalHostedZoneId` are the alias target for |
| 178 | regional endpoints (edge-optimized domains expose |
| 179 | `distributionDomainName` / `distributionHostedZoneId` instead). The `.as<string>()` |
| 180 | narrows the attribute's type — API Gateway only populates the regional fields |
| 181 | for regional endpoint configurations. |
| 182 | |
| 183 | ## Where next |
| 184 | |
| 185 | - [Websites on AWS](/aws/frontend/websites) — the `domain` prop in the context of the |
| 186 | full websites surface |
| 187 | - [Deploy a static site](/aws/frontend/static-site) — Router composition and |
| 188 | cache invalidation |
| 189 | - [REST API (API Gateway v1)](/aws/apis/api-gateway) — the API this |
| 190 | domain fronts |
| 191 | - Reference: [Certificate](/providers/aws/acm/certificate), |
| 192 | [Record](/providers/aws/route53/record), |
| 193 | [HostedZone](/providers/aws/route53/hostedzone), |
| 194 | [DomainName](/providers/aws/apigateway/domainname) |