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/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",});Cloudflare treats (name, type) as a record’s identity: changing
content, ttl, proxied, or comment patches the record in
place, while changing name or type replaces it (a new record
is created, then the old one deleted).
Records get the same adoption safety as zones. If a record with
the same (name, type) 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). This is
deliberate: DNS is where “already configured in the dashboard”
lives, and overwriting it silently would be the worst possible
default.
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:
Reference: