Skip to content

Cloudflare.DNS reference

Source: packages/cloudflare/src/DNS/AccountSettings.ts

The DNS settings of a Cloudflare account (/accounts/{account_id}/dns_settings) — the account-wide enforceDnsOnly override and the default DNS settings applied to every new zone (zoneDefaults).

The settings object is a per-account singleton — it always exists with Cloudflare defaults, so this resource never creates or deletes anything physical. Reconcile patches only the fields you declare (and only when the observed value differs); destroy restores the managed fields to the values they had before Alchemy first touched the account (captured as initialSettings).

Some fields are plan-gated: zoneDefaults.nsTtl and custom SOA values require the custom nameserver TTL / custom SOA entitlements, nameservers.type: "cloudflare.advanced" (Foundation DNS Advanced Nameservers) is a paid add-on, and internalDns is Enterprise Internal DNS only.

AccountDnsSettings: Account-wide overrides

Section titled “AccountDnsSettings: Account-wide overrides”
yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", {
enforceDnsOnly: true,
});

Flatten CNAMEs in every new zone

yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", {
zoneDefaults: { flattenAllCnames: true },
});

Default new zones to multi-provider DNS

yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", {
zoneDefaults: { multiProvider: true },
});

Default new zones to Advanced Nameservers

// Paid add-on. Replaces the removed `zoneDefaults.foundationDns: true`.
yield* Cloudflare.DNS.AccountDnsSettings("DnsSettings", {
zoneDefaults: { nameservers: { type: "cloudflare.advanced" } },
});

Source: packages/cloudflare/src/DNS/Dnssec.ts

DNSSEC configuration for a Cloudflare zone (/zones/{zone_id}/dnssec).

DNSSEC is a per-zone singleton — it always exists in either an enabled or disabled state, so this resource never creates or deletes anything physical. Reconcile patches the configuration toward the desired state; destroy restores the state the zone had before Alchemy first managed it (enabled stays enabled, previously-disabled zones are deactivated again).

Activation is eventually consistent: after enabling, Cloudflare reports pending until the ds attribute (the DS record) is submitted at the domain’s registrar. The reconciler polls with bounded retries for the zone to leave the disabled state but does not wait for full active — that depends on the registrar.

Safety: when there is no prior state and DNSSEC is already enabled on the zone, read reports it as Unowned and the engine refuses to take it over unless --adopt (or adopt(true)) is set.

Sign the zone

const dnssec = yield* Cloudflare.DNS.Dnssec("ZoneDnssec", {
zoneId: zone.zoneId,
});
// Paste `dnssec.ds` at your registrar to complete activation.

Multi-signer DNSSEC

yield* Cloudflare.DNS.Dnssec("ZoneDnssec", {
zoneId: zone.zoneId,
dnssecMultiSigner: true,
});
yield* Cloudflare.DNS.Dnssec("ZoneDnssec", {
zoneId: zone.zoneId,
status: "disabled",
});

Source: packages/cloudflare/src/DNS/Firewall.ts

A Cloudflare DNS Firewall cluster.

DNS Firewall sits in front of your authoritative DNS infrastructure, caching responses on Cloudflare’s anycast network and shielding the upstream nameservers from attack traffic. Creating a cluster assigns a set of Cloudflare anycast IPs (dnsFirewallIps) that you point NS glue records at; queries hitting those IPs are answered from cache or forwarded to your upstreamIps.

DNS Firewall is a paid add-on (typically Enterprise / contract). On accounts without the entitlement, creation fails with the typed DnsFirewallNotEntitled error (Cloudflare error code 10101).

All settings are mutable in place; only name (the cold-state recovery identity) triggers a replacement.

Basic cluster

const cluster = yield* Cloudflare.DNS.Firewall("dns-shield", {
upstreamIps: ["192.0.2.1", "192.0.2.2"],
});
// Point NS glue records at the assigned anycast IPs:
const ips = cluster.dnsFirewallIps;

Tuned caching and attack mitigation

const cluster = yield* Cloudflare.DNS.Firewall("dns-shield", {
upstreamIps: ["192.0.2.1"],
minimumCacheTtl: 120,
maximumCacheTtl: 3600,
negativeCacheTtl: 300,
ratelimit: 600,
retries: 2,
attackMitigation: {
enabled: true,
onlyWhenUpstreamUnhealthy: true,
},
});
const cluster = yield* Cloudflare.DNS.Firewall("dns-shield", {
upstreamIps: ["192.0.2.1"],
reverseDns: {
"203.0.113.1": "ns1.example.com",
},
});

Source: packages/cloudflare/src/DNS/ReadDns.ts

Binding that lets a Worker read Cloudflare DNS records at runtime.

Creates a least-privilege AccountApiToken with only the DNS Read permission, scoped to the single zone passed to bind, and binds its value into the Worker so runtime code can authenticate.

Bind the client in the Worker’s Init phase and provide ReadDnsBinding. The zone is fixed by ReadDnsBinding(zone) — the provisioned token only grants access to that zone, so calls take no zoneId. Pass the Zone resource directly (it’s an Effect), or yield* Zone for a resolved value.

import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
const Zone = Cloudflare.Zone.Zone("MyZone", { name: "example.com" });
export class ReadDnserWorker extends Cloudflare.Worker<ReadDnserWorker>()(
"ReadDnserWorker",
{ main: import.meta.url },
Effect.gen(function* () {
// Init phase — bind the read client scoped to the zone.
const dns = yield* Cloudflare.DNS.ReadDns(Zone);
return {
fetch: Effect.gen(function* () {
const { result } = yield* dns.listDnsRecords({ type: "A" });
const record = yield* dns.getDnsRecord(result[0].id);
return yield* HttpServerResponse.json({ id: record.id });
}),
};
}).pipe(Effect.provide(Cloudflare.DNS.ReadDnsBinding)),
) {}

Source: packages/cloudflare/src/DNS/ReadWriteDns.ts

Binding that lets a Worker perform the full Cloudflare DNS record CRUD surface at runtime.

Creates a least-privilege AccountApiToken with both the DNS Read and DNS Write permissions, scoped to the single zone passed to bind, and binds its value into the Worker so runtime code can authenticate.

ReadWriteDns: Managing DNS records at runtime

Section titled “ReadWriteDns: Managing DNS records at runtime”

Bind the client in the Worker’s Init phase and provide ReadWriteDnsBinding. The zone is fixed by ReadWriteDnsBinding(zone) — the provisioned token only grants access to that zone, so calls take no zoneId. Pass the Zone resource directly (it’s an Effect), or yield* Zone for a resolved value.

import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
const Zone = Cloudflare.Zone.Zone("MyZone", { name: "example.com" });
export class Worker extends Cloudflare.Worker<Worker>()(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
// Init phase — bind the full CRUD client scoped to the zone.
const dns = yield* Cloudflare.DNS.ReadWriteDns(Zone);
return {
fetch: Effect.gen(function* () {
const { result } = yield* dns.createDnsRecord({
type: "A",
name: "app.example.com",
content: "192.0.2.1",
ttl: 1,
});
const record = yield* dns.getDnsRecord(result.id);
yield* dns.deleteDnsRecord(result.id);
return yield* HttpServerResponse.json({ id: record.id });
}),
};
}).pipe(Effect.provide(Cloudflare.DNS.ReadWriteDnsBinding)),
) {}

Source: packages/cloudflare/src/DNS/Record.ts

A single DNS record on a Cloudflare-managed zone.

Once created or adopted, the record is tracked by its Cloudflare record id: changing its name, type or content updates it in place.

Safety: when there is no prior state, read looks for an existing record with the same name, type and content (and priority). Only an exact match is this record. It is reported as Unowned, and the engine refuses to take it over unless --adopt (or adopt(true)) is set. Any other record at the same name is left alone and a new record is created beside it, so a second TXT, MX or A record never takes over an existing one. To adopt a record and then change it, declare its current content/priority verbatim first, then change them in a follow-up deploy. A CNAME is the exception: a name holds only one, so an existing CNAME is adopted whatever it points at.

Record: Proxied CNAME pointing at a tunnel

Section titled “Record: Proxied CNAME pointing at a tunnel”
yield* Cloudflare.DNS.Record("AdminCname", {
zoneId: zone.zoneId,
name: "cluster-admin.example.com",
type: "CNAME",
content: `${tunnel.tunnelId}.cfargotunnel.com`,
proxied: true,
comment: "research admin UI",
});
yield* Cloudflare.DNS.Record("ApiA", {
zoneId: zone.zoneId,
name: "api.example.com",
type: "A",
content: "203.0.113.42",
ttl: 300,
});

Record: Structured service binding records

Section titled “Record: Structured service binding records”

SVCB record

yield* Cloudflare.DNS.Record("McpSvcb", {
zoneId: zone.zoneId,
name: "_mcp._agents.example.com",
type: "SVCB",
content: {
priority: 1,
target: "mcp.example.com.",
value: 'mandatory="alpn,port" alpn="h2,h3" port="443"',
},
});

HTTPS record

yield* Cloudflare.DNS.Record("WebsiteHttps", {
zoneId: zone.zoneId,
name: "example.com",
type: "HTTPS",
content: {
priority: 1,
target: ".",
value: 'alpn="h2,h3"',
},
});

Source: packages/cloudflare/src/DNS/RecordList.ts

An explicit list of Cloudflare DNS records whose values are typically computed from another resource’s outputs — ACM certificate validation CNAMEs, a platform’s ownership-verification TXT, a Fly App’s addresses.

This is the resource a Cloudflare.DNS.Adapter() declares for domain.dns record publication (see DNS Adapters). Records are DNS-only, the zone of each record is inferred from its name unless zone is set, and only the records the list published are removed when they leave the list or the list is destroyed.

Certificate Validation Records

const cert = yield* AWS.ACM.Certificate("Cert", {
domainName: "app.example.com",
dnsValidation: "external",
});
yield* Cloudflare.DNS.RecordList("CertValidation", {
records: cert.domainValidationOptions.pipe(
Output.map(AWS.ACM.validationRecordsOf),
),
});

Verification TXT

yield* Cloudflare.DNS.RecordList("Verify", {
zone: "example.com",
records: [
{ name: "_verify.example.com", type: "TXT", value: "token-123" },
],
});

Source: packages/cloudflare/src/DNS/Records.ts

A dynamic set of identically-configured Cloudflare DNS records that differ only by name — the Cloudflare counterpart of AWS.Route53.Records.

Unlike Cloudflare.DNS.Record (one resource per record), Records reconciles a whole name set against the zone: names added to the set are created or overwritten, names removed from the set are deleted. The set is the union of the declared names prop and names contributed through the RecordsBinding binding contract, which is how composites (e.g. a site attached to an AWS.Website.Router) register hostnames on a distribution’s DNS without a circular input prop.

Ownership: when the set is first created, records that already exist at its names are reported as not owned, so the deploy refuses to overwrite them unless --adopt (or adopt(true)) is set. Names added later are overwritten like a Route 53 UPSERT, and removed again when the name leaves the set or the set is destroyed. Cloudflare rejects a CNAME at a name that already has A/AAAA records; remove those first.

CNAMEs For Several Hostnames

const records = yield* Cloudflare.DNS.Records("AliasRecords", {
type: "CNAME",
content: distribution.domainName,
names: ["www.example.com", "docs.example.com"],
});

Binding Target For Composite-Contributed Names

// An empty set that attached sites bind their hostnames onto:
const records = yield* Cloudflare.DNS.Records("SiteAliasRecords", {
zone: "example.com",
type: "CNAME",
content: distribution.domainName,
});
// elsewhere:
yield* records.bind`MySite`({ names: ["docs.example.com"] });

Source: packages/cloudflare/src/DNS/View.ts

An Internal DNS view (/accounts/{account_id}/dns_settings/views) — a named set of internal zones that DNS queries can be resolved against, for split-horizon / internal DNS setups.

Requires the Enterprise Internal DNS entitlement on the account (creation fails with InternalDnsNotAvailable otherwise). Both name and zones are mutable in place.

View over internal zones

const view = yield* Cloudflare.DNS.View("Internal", {
zones: [internalZone.zoneId],
});

View with an explicit name

const view = yield* Cloudflare.DNS.View("Internal", {
name: "datacenter-east",
zones: [zoneA.zoneId, zoneB.zoneId],
});

Source: packages/cloudflare/src/DNS/WriteDns.ts

Binding that lets a Worker create, update, and delete Cloudflare DNS records at runtime.

Creates a least-privilege AccountApiToken with only the DNS Write permission, scoped to the single zone passed to bind, and binds its value into the Worker so runtime code can authenticate.

Create, update, and delete records from inside a Worker

Bind the client in the Worker’s Init phase and provide WriteDnsBinding. The zone is fixed by WriteDnsBinding(zone) — the provisioned token only grants access to that zone, so calls take no zoneId. Pass the Zone resource directly (it’s an Effect), or yield* Zone for a resolved value.

import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
const Zone = Cloudflare.Zone.Zone("MyZone", { name: "example.com" });
export class WriteDnsrWorker extends Cloudflare.Worker<WriteDnsrWorker>()(
"WriteDnsrWorker",
{ main: import.meta.url },
Effect.gen(function* () {
// Init phase — bind the write client scoped to the zone.
const dns = yield* Cloudflare.DNS.WriteDns(Zone);
return {
fetch: Effect.gen(function* () {
const { result } = yield* dns.createDnsRecord({
type: "A",
name: "app.example.com",
content: "192.0.2.1",
ttl: 1,
proxied: true,
});
yield* dns.updateDnsRecord(result.id, {
type: "A",
name: "app.example.com",
content: "192.0.2.2",
ttl: 1,
});
yield* dns.deleteDnsRecord(result.id);
return yield* HttpServerResponse.json({ id: result.id });
}),
};
}).pipe(Effect.provide(Cloudflare.DNS.WriteDnsBinding)),
) {}

Apply a batch of changes atomically

yield* dns.batchDnsRecords({
posts: [{ type: "A", name: "a.example.com", content: "192.0.2.1", ttl: 1 }],
deletes: [{ id: oldRecordId }],
});

Source: packages/cloudflare/src/DNS/ZoneSettings.ts

The DNS settings of a Cloudflare zone (/zones/{zone_id}/dns_settings) — nameserver assignment, NS TTL, SOA components, CNAME flattening, multi-provider mode, and zone mode.

The settings object is a per-zone singleton — it always exists with Cloudflare defaults, so this resource never creates or deletes anything physical. Reconcile patches only the fields you declare (and only when the observed value differs); destroy restores the managed fields to the values they had before Alchemy first touched the zone (captured as initialSettings).

Some fields are plan-gated: nameservers.type: "cloudflare.advanced" (Foundation DNS Advanced Nameservers) is a paid add-on, nameservers.type: "custom.*" requires account custom nameservers, internalDns and secondaryOverrides are Enterprise features.

Lower the NS record TTL

yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
zoneId: zone.zoneId,
nsTtl: 3600,
});

Flatten every CNAME in the zone

yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
zoneId: zone.zoneId,
flattenAllCnames: true,
});
yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
zoneId: zone.zoneId,
soa: { minTtl: 300 },
});
yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
zoneId: zone.zoneId,
multiProvider: true,
});
// Paid add-on. Replaces the removed `foundationDns: true`.
yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
zoneId: zone.zoneId,
nameservers: { type: "cloudflare.advanced" },
});

Source: packages/cloudflare/src/DNS/ZoneTransferAcl.ts

A Secondary DNS zone-transfer ACL (/accounts/{account_id}/secondary_dns/acls) — an account-wide IPv4/IPv6 range that may receive NOTIFYs for secondary zones and from which Cloudflare accepts AXFR/IXFR requests for outgoing transfers.

Requires the Secondary DNS (zone transfer) entitlement on the account. Both name and ipRange are mutable in place.

Allow a primary nameserver range

const acl = yield* Cloudflare.DNS.ZoneTransferAcl("PrimaryNs", {
ipRange: "192.0.2.48/28",
});

ACL with an explicit name

const acl = yield* Cloudflare.DNS.ZoneTransferAcl("PrimaryNs", {
name: "primary-nameservers",
ipRange: "2001:db8::/64",
});

Source: packages/cloudflare/src/DNS/ZoneTransferIncoming.ts

The incoming zone-transfer configuration of a secondary zone (/zones/{zone_id}/secondary_dns/incoming) — links the zone to the peers Cloudflare transfers it in from and sets the auto-refresh interval.

Requires the Secondary DNS (zone transfer) entitlement, and the zone must be created with type: "secondary". The configuration is a per-zone singleton: zoneId is the identity (replacement on change), everything else is mutable in place.

ZoneTransferIncoming: Configuring incoming transfers

Section titled “ZoneTransferIncoming: Configuring incoming transfers”
const peer = yield* Cloudflare.DNS.ZoneTransferPeer("Primary", {
ip: "192.0.2.53",
port: 53,
});
yield* Cloudflare.DNS.ZoneTransferIncoming("Incoming", {
zoneId: zone.zoneId,
name: "example.com.",
peers: [peer.peerId],
autoRefreshSeconds: 86400,
});

Source: packages/cloudflare/src/DNS/ZoneTransferOutgoing.ts

The outgoing zone-transfer configuration of a primary zone (/zones/{zone_id}/secondary_dns/outgoing) — links the zone to the peers Cloudflare NOTIFYs and serves AXFR/IXFR to, and toggles transfers on or off via the dedicated enable/disable endpoints.

Requires the Secondary DNS (zone transfer) entitlement on the zone. The configuration is a per-zone singleton: zoneId is the identity (replacement on change), everything else is mutable in place.

ZoneTransferOutgoing: Configuring outgoing transfers

Section titled “ZoneTransferOutgoing: Configuring outgoing transfers”

Serve a primary zone to an external secondary

const peer = yield* Cloudflare.DNS.ZoneTransferPeer("Secondary", {
ip: "192.0.2.53",
port: 53,
});
yield* Cloudflare.DNS.ZoneTransferOutgoing("Outgoing", {
zoneId: zone.zoneId,
name: "example.com.",
peers: [peer.peerId],
});

Configure transfers but keep them disabled

yield* Cloudflare.DNS.ZoneTransferOutgoing("Outgoing", {
zoneId: zone.zoneId,
name: "example.com.",
peers: [peer.peerId],
enabled: false,
});

Source: packages/cloudflare/src/DNS/ZoneTransferPeer.ts

A Secondary DNS zone-transfer peer (/accounts/{account_id}/secondary_dns/peers) — an external nameserver Cloudflare exchanges zone transfers with. Link peers to a zone via ZoneTransferIncoming (secondary zones) or ZoneTransferOutgoing (primary zones).

Requires the Secondary DNS (zone transfer) entitlement on the account. Cloudflare’s create API only accepts a name; the provider follows up with an update when ip, port, tsigId, or ixfrEnable are declared, all of which remain mutable in place.

Primary nameserver to transfer from

const peer = yield* Cloudflare.DNS.ZoneTransferPeer("Primary", {
ip: "192.0.2.53",
port: 53,
});

Peer with TSIG authentication

const tsig = yield* Cloudflare.DNS.ZoneTransferTsig("TransferKey", {
algo: "hmac-sha512.",
secret: Redacted.make(process.env.TSIG_SECRET!),
});
const peer = yield* Cloudflare.DNS.ZoneTransferPeer("Primary", {
ip: "192.0.2.53",
tsigId: tsig.tsigId,
ixfrEnable: true,
});

ZoneTransferPeer: Looking up an existing peer

Section titled “ZoneTransferPeer: Looking up an existing peer”

ZoneTransferPeer.lookup references a peer Alchemy doesn’t manage (created in the dashboard or by another stack), found by peerId or name. It is read during plan/deploy and never created, updated or deleted.

const secondary = Cloudflare.DNS.ZoneTransferPeer.lookup({ name: "secondary" });
yield* Cloudflare.DNS.ZoneTransferOutgoing("Outgoing", {
zoneId: zone.zoneId,
name: zone.name,
peers: [secondary.peerId],
});

Source: packages/cloudflare/src/DNS/ZoneTransferTsig.ts

A Secondary DNS TSIG key (/accounts/{account_id}/secondary_dns/tsigs) — shared-secret authentication for zone transfers between Cloudflare and external nameservers. Reference it from a ZoneTransferPeer via tsigId.

Requires the Secondary DNS (zone transfer) entitlement on the account. All fields are mutable in place; the secret is redacted and never persisted in attributes.

const tsig = yield* Cloudflare.DNS.ZoneTransferTsig("TransferKey", {
algo: "hmac-sha512.",
secret: Redacted.make(process.env.TSIG_SECRET!),
});
const peer = yield* Cloudflare.DNS.ZoneTransferPeer("Primary", {
ip: "192.0.2.53",
port: 53,
tsigId: tsig.tsigId,
});

ZoneTransferTsig: Looking up an existing key

Section titled “ZoneTransferTsig: Looking up an existing key”

ZoneTransferTsig.lookup references a TSIG key Alchemy doesn’t manage (created in the dashboard or by another stack), found by tsigId or name. It is read during plan/deploy and never created, updated or deleted. Its secret is never read.

const tsig = Cloudflare.DNS.ZoneTransferTsig.lookup({ name: "transfer-key" });
const peer = yield* Cloudflare.DNS.ZoneTransferPeer("Primary", {
ip: "192.0.2.53",
tsigId: tsig.tsigId,
});