Skip to content

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.

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());

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.

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.

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.

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.

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.

Reference: