1---
2title: Domains & DNS
3description: Manage Cloudflare zones, DNS records, DNSSEC, and zone settings as resources — and safely adopt the domains you already have.
4---
5
6Your domain is infrastructure too. Alchemy manages Cloudflare
7zones, the DNS records inside them, DNSSEC, and per-zone settings
8as ordinary resources — declared next to the Workers they route
9to, diffed and converged on every deploy.
10
11Most of the time you won't create a zone from scratch: the domain
12already exists in your Cloudflare account, hand-configured over
13the years. Adoption is designed for exactly that — Alchemy takes
14over an existing zone or record only when you explicitly opt in,
15so a deploy can never clobber DNS it doesn't own.
16
17## Create a zone
18
19A zone is one resource declaration:
20
21```typescript
22import * as Cloudflare from "alchemy/Cloudflare";
23
24const zone = yield* Cloudflare.Zone.Zone("MyZone", {
25 name: "example.com",
26});
27// zone.zoneId, zone.nameServers, zone.status, ...
28```
29
30Cloudflare assigns name servers to the new zone
31(`zone.nameServers`); point your registrar at them and the zone
32moves from `pending` to `active` once Cloudflare detects proof of
33ownership.
34
35Zones default to **retain**: destroying the stack does *not*
36delete the zone in Cloudflare. Opt in to actual deletion with
37`destroy()`:
38
39```typescript
40import { destroy } from "alchemy/RemovalPolicy";
41
42yield* Cloudflare.Zone.Zone("MyZone", {
43 name: "example.com",
44}).pipe(destroy());
45```
46
47## Adopt an existing zone
48
49A Cloudflare zone carries no ownership markers, so Alchemy cannot
50prove it created a zone that already exists. Deploying a `Zone`
51whose name matches a pre-existing zone fails with an
52`OwnedBySomeoneElse` error — unless you opt in with `adopt(true)`:
53
54```typescript
55import { adopt } from "alchemy/AdoptPolicy";
56
57const zone = yield* Cloudflare.Zone.Zone("MyZone", {
58 name: "example.com",
59}).pipe(adopt(true));
60// zone.zoneId, zone.nameServers, zone.accountId, ...
61```
62
63The engine takes over the existing zone instead of creating a new
64one, and from then on manages it like any other resource. The
65same gate can be lifted for an entire deploy with
66`alchemy deploy --adopt`. How
67ownership gating works under the hood is covered in
68[Concepts › Provider](/infrastructure-as-code/provider).
69
70## DNS records
71
72`Cloudflare.DNS.Record` manages a single record in a zone. A
73plain A record:
74
75```typescript
76yield* Cloudflare.DNS.Record("ApiA", {
77 zoneId: zone.zoneId,
78 name: "api.example.com",
79 type: "A",
80 content: "203.0.113.42",
81 ttl: 300,
82});
83```
84
85Set `proxied: true` to send traffic through Cloudflare's proxy
86(orange-clouded in the dashboard). Proxied records use
87Cloudflare's automatic TTL — the default when `ttl` is omitted —
88and only `A`, `AAAA`, and `CNAME` records are proxiable:
89
90```typescript
91yield* Cloudflare.DNS.Record("AppCname", {
92 zoneId: zone.zoneId,
93 name: "app.example.com",
94 type: "CNAME",
95 content: `${tunnel.tunnelId}.cfargotunnel.com`,
96 proxied: true,
97 comment: "app behind a Cloudflare Tunnel",
98});
99```
100
101Cloudflare treats `(name, type)` as a record's identity: changing
102`content`, `ttl`, `proxied`, or `comment` patches the record in
103place, while changing `name` or `type` replaces it (a new record
104is created, then the old one deleted).
105
106Records get the same adoption safety as zones. If a record with
107the same `(name, type)` already exists in the zone — a
108hand-edited apex `A` record, an email DKIM/SPF entry — the deploy
109refuses to take it over unless you pipe `adopt(true)`. This is
110deliberate: DNS is where "already configured in the dashboard"
111lives, and overwriting it silently would be the worst possible
112default.
113
114## Worker custom domains
115
116This is the layer under a Worker's `domain` prop. When you write:
117
118```typescript
119export default Cloudflare.Worker(
120 "Worker",
121 {
122 main: import.meta.url,
123 domain: "app.example.com",
124 },
125 // ...
126);
127```
128
129Cloudflare infers the zone from the hostname — the zone must
130already exist in the account. Declaring the `Zone` resource in
131the same stack (or adopting the existing one, as above) is what
132satisfies that requirement end-to-end. The
133[custom domains guide](/cloudflare/networking/custom-domains) walks
134through the full setup: zone, DNS, and Worker wired together.
135
136## Zone settings and DNSSEC
137
138`Cloudflare.Zone.Setting` pins a single zone setting to a desired
139value. Settings are singletons — they always exist with a
140Cloudflare default — so the resource patches the value on deploy
141and restores the original on destroy:
142
143```typescript
144yield* Cloudflare.Zone.Setting("AlwaysUseHttps", {
145 zoneId: zone.zoneId,
146 settingId: "always_use_https",
147 value: "on",
148});
149```
150
151`Cloudflare.DNS.Dnssec` signs the zone. Activation completes at
152your registrar: paste the `ds` attribute (the DS record) there,
153and Cloudflare reports `pending` until it's in place:
154
155```typescript
156const dnssec = yield* Cloudflare.DNS.Dnssec("ZoneDnssec", {
157 zoneId: zone.zoneId,
158});
159// Paste `dnssec.ds` at your registrar to complete activation.
160```
161
162Like zone settings, DNSSEC is a per-zone singleton: destroying
163the resource restores whatever state the zone had before Alchemy
164first managed it.
165
166## Where next
167
168Guides:
169
170- [Custom domains](/cloudflare/networking/custom-domains) — serve a
171 Worker from your own hostname, end to end.
172
173Related:
174
175- [Workers](/cloudflare/compute/workers) — the compute your DNS points
176 at.
177- [Email](/cloudflare/email) — route mail on the zone and send from
178 Workers.
179
180Reference:
181
182- [Zone API reference](/providers/cloudflare/zone/zone)
183- [Record API reference](/providers/cloudflare/dns/record)
184- [Setting API reference](/providers/cloudflare/zone/setting)
185- [Dnssec API reference](/providers/cloudflare/dns/dnssec)