1---
2title: Custom domains with Route53 + ACM
3description: Serve your site or API from your own domain — DNS-validated ACM certificates, Route 53 alias records, and the domain prop on Website resources.
4sidebar:
5 order: 6
6---
7
8A custom domain on AWS is three moving parts: a TLS certificate (ACM), DNS
9records (Route 53), and wiring both into whatever serves traffic (CloudFront
10or 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
20const 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
32With `hostedZoneId` set, Alchemy requests a DNS-validated ACM certificate
33covering `name` plus every alias, upserts the validation records into your
34zone, waits for issuance, attaches the certificate to the CloudFront
35distribution, and creates an `A` alias record for each hostname. The
36resource's `url` output becomes `https://app.example.com`. When a site is
37attached to a `Router`, put `domain` on the Router — passing it to the
38attached `StaticSite` as well is an error.
39
40## Bring your own certificate
41
42If you already have an ACM certificate, pass its ARN as `cert` and Alchemy
43skips certificate creation:
44
45```typescript
46domain: {
47 name: "app.example.com",
48 hostedZoneId: "Z1234567890",
49 cert: existingCertificateArn,
50},
51```
52
53DNS records are still managed for you — only the certificate step is
54replaced. The certificate must cover the domain name and every alias you
55list.
56
57## Manage DNS elsewhere
58
59Set `dns: false` to skip Route 53 entirely:
60
61```typescript
62domain: {
63 name: "app.example.com",
64 cert: existingCertificateArn,
65 dns: false,
66},
67```
68
69No records are created, so you point your DNS provider at the distribution's
70domain name yourself. `dns: false` requires `cert` — without Route 53 access
71Alchemy can't complete DNS validation for a new certificate, so you must
72bring one.
73
74## Create or reference a hosted zone
75
76The `domain` prop and the primitives below both key off a hosted zone ID. If
77the zone lives outside your stack, pass its ID as a plain string; to manage
78it in the stack, use `AWS.Route53.HostedZone`:
79
80```typescript
81const 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`
88records at your registrar (or parent zone) to delegate the domain. Set
89`forceDestroy: true` if you want `destroy` to delete any remaining records
90before 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
98const cert = yield* AWS.ACM.Certificate("WebsiteCertificate", {
99 domainName: "app.example.com",
100 subjectAlternativeNames: ["www.app.example.com"],
101 hostedZoneId: zone.id,
102});
103```
104
105The provider upserts the DNS validation records ACM asks for into the zone,
106waits for Route 53 to propagate them, and blocks until the certificate
107reaches `ISSUED` — so `cert.certificateArn` is ready to attach the moment
108the effect resolves. Certificates are requested in `us-east-1`, the region
109CloudFront requires for viewer certificates. ACM certificates are immutable:
110changing the domain name, SANs, or validation settings replaces the
111certificate 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
119yield* 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
130Note that `aliasTarget.hostedZoneId` is the *distribution's* hosted zone ID
131(an attribute CloudFront exposes), not your own zone's. Records are written
132with `UPSERT` — create and update are the same operation — and the provider
133waits for the Route 53 change to reach `INSYNC` before returning, so the
134record is live when the deploy finishes.
135
136## Custom domain for API Gateway
137
138For a REST API, create an `AWS.ApiGateway.DomainName` with a certificate and
139map your API's stage onto it with a `BasePathMapping`:
140
141```typescript
142const 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
149yield* AWS.ApiGateway.BasePathMapping("Root", {
150 domainName: domain.domainName,
151 restApiId: api.restApiId,
152 stage: "prod",
153});
154```
155
156One region caveat: a `REGIONAL` domain needs its certificate in the API's own
157region, while `AWS.ACM.Certificate` always issues in `us-east-1`. Deploying
158to `us-east-1` works as written; in any other region, pass the ARN of a
159certificate that already exists there as `regionalCertificateArn`.
160
161## Point DNS at the API
162
163The `DomainName` resource exposes the regional endpoint to alias to:
164
165```typescript
166yield* 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
178regional endpoints (edge-optimized domains expose
179`distributionDomainName` / `distributionHostedZoneId` instead). The `.as<string>()`
180narrows the attribute's type — API Gateway only populates the regional fields
181for 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)