Domains & DNS
Your domain is infrastructure too. Alchemy manages Cloudflare zones, the DNS records inside them, DNSSEC, and per-zone settings as ordinary resources — declared next to the Workers they route to, diffed and converged on every deploy.
Most of the time you won’t create a zone from scratch: the domain already exists in your Cloudflare account, hand-configured over the years. Adoption is designed for exactly that — Alchemy takes over an existing zone or record only when you explicitly opt in, so a deploy can never clobber DNS it doesn’t own.
Create a zone
Section titled “Create a zone”A zone is one resource declaration:
import * as Cloudflare from "@alchemy.run/cloudflare";
const zone = yield* Cloudflare.Zone.Zone("MyZone", { name: "example.com",});// zone.zoneId, zone.nameServers, zone.status, ...Cloudflare assigns name servers to the new zone
(zone.nameServers); point your registrar at them and the zone
moves from pending to active once Cloudflare detects proof of
ownership.
Zones default to retain: destroying the stack does not
delete the zone in Cloudflare. Opt in to actual deletion with
destroy():
import { destroy } from "alchemy/RemovalPolicy";
yield* Cloudflare.Zone.Zone("MyZone", { name: "example.com",}).pipe(destroy());Adopt an existing zone
Section titled “Adopt an existing zone”A Cloudflare zone carries no ownership markers, so Alchemy cannot
prove it created a zone that already exists. Deploying a Zone
whose name matches a pre-existing zone fails with an
OwnedBySomeoneElse error — unless you opt in with adopt(true):
import { adopt } from "alchemy/AdoptPolicy";
const zone = yield* Cloudflare.Zone.Zone("MyZone", { name: "example.com",}).pipe(adopt(true));// zone.zoneId, zone.nameServers, zone.accountId, ...The engine takes over the existing zone instead of creating a new
one, and from then on manages it like any other resource. The
same gate can be lifted for an entire deploy with
alchemy deploy --adopt. How
ownership gating works under the hood is covered in
Concepts › Provider.
DNS records
Section titled “DNS records”Cloudflare.DNS.Record manages a single record in a zone. A
plain A record:
yield* Cloudflare.DNS.Record("ApiA", { zoneId: zone.zoneId, name: "api.example.com", type: "A", content: "203.0.113.42", ttl: 300,});Set proxied: true to send traffic through Cloudflare’s proxy
(orange-clouded in the dashboard). Proxied records use
Cloudflare’s automatic TTL — the default when ttl is omitted —
and only A, AAAA, and CNAME records are proxiable:
yield* Cloudflare.DNS.Record("AppCname", { zoneId: zone.zoneId, name: "app.example.com", type: "CNAME", content: `${tunnel.tunnelId}.cfargotunnel.com`, proxied: true, comment: "app behind a Cloudflare Tunnel",});A record is tracked by its Cloudflare record id, so changing its
name, type, content, ttl, proxied, or comment updates it
in place. Only moving it to another zone replaces it. Unless you set
comment, Alchemy labels each record Managed by Alchemy (<FQN>) in
the dashboard.
Records get the same adoption safety as zones. If a record with the
same name, type and content (and priority, for MX and URI)
already exists in the zone — a hand-edited apex A record, an email
DKIM/SPF entry — the deploy refuses to take it over unless you pipe
adopt(true). A record with different content is left alone and a
new one is created beside it, so adding a second TXT or MX record
never overwrites the first. For an A or AAAA record that means
traffic is split between the old and new address until you remove
the old record or adopt it. To adopt a record and then change it,
declare its current content first, then change it in a later
deploy. A CNAME is the exception: a name holds only one, so
adopt(true) takes over an existing CNAME whatever it points at.
Worker custom domains
Section titled “Worker custom domains”This is the layer under a Worker’s domain prop. When you write:
export default Cloudflare.Worker( "Worker", { main: import.meta.url, domain: "app.example.com", }, // ...);Cloudflare infers the zone from the hostname — the zone must
already exist in the account. Declaring the Zone resource in
the same stack (or adopting the existing one, as above) is what
satisfies that requirement end-to-end. The
custom domains guide walks
through the full setup: zone, DNS, and Worker wired together.
Zone settings and DNSSEC
Section titled “Zone settings and DNSSEC”Cloudflare.Zone.Setting pins a single zone setting to a desired
value. Settings are singletons — they always exist with a
Cloudflare default — so the resource patches the value on deploy
and restores the original on destroy:
yield* Cloudflare.Zone.Setting("AlwaysUseHttps", { zoneId: zone.zoneId, settingId: "always_use_https", value: "on",});Cloudflare.DNS.Dnssec signs the zone. Activation completes at
your registrar: paste the ds attribute (the DS record) there,
and Cloudflare reports pending until it’s in place:
const dnssec = yield* Cloudflare.DNS.Dnssec("ZoneDnssec", { zoneId: zone.zoneId,});// Paste `dnssec.ds` at your registrar to complete activation.Like zone settings, DNSSEC is a per-zone singleton: destroying the resource restores whatever state the zone had before Alchemy first managed it.
Where next
Section titled “Where next”Guides:
- Custom domains — serve a Worker from your own hostname, end to end.
Related:
- Workers — the compute your DNS points at.
- Email — route mail on the zone and send from Workers.
- DNS Adapters — publish another platform’s custom-domain records (AWS, Fly, Railway, …) in a Cloudflare zone.
Reference: