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