1---
2title: DNS validation
3description: Configure Cloudflare DNS-01 challenges, wildcard certificates, propagation, and custom solvers.
4---
5
6DNS-01 proves control of a name by publishing a CA-derived TXT value under
7`_acme-challenge`. The CA reads that value before issuing the certificate.
8Alchemy implements DNS-01 only; your application does not need a public HTTP
9challenge endpoint.
10
11## Reference a Cloudflare zone
12
13Inside the stack from [Getting started](/acme/getting-started):
14
15```typescript
16const certificate = yield* ACME.Certificate("Site", {
17 account,
18 identifiers: ["example.com"],
19 solver: Cloudflare.DNS.AcmeSolver({
20 zoneId: Config.String("CLOUDFLARE_ZONE_ID"),
21 }),
22});
23```
24
25`AcmeSolver` returns serializable data containing the solver type and zone ID.
26Include both `ACME.providers()` and `Cloudflare.providers()` in the stack. The
27Cloudflare credentials must allow listing, creating, and deleting TXT records in
28that zone. The domain must already be delegated to that zone's nameservers.
29
30## Use a managed zone instead
31
32If your stack already owns a Cloudflare `Zone` resource, pass the resolved zone:
33
34```diff lang="typescript"
35+const zone = yield* Zone;
36 const certificate = yield* ACME.Certificate("Site", {
37 account,
38 identifiers: ["example.com"],
39- solver: Cloudflare.DNS.AcmeSolver({
40- zoneId: Config.String("CLOUDFLARE_ZONE_ID"),
41- }),
42+ solver: Cloudflare.DNS.AcmeSolver(zone),
43 });
44```
45
46The zone's output supplies the ID and establishes the resource dependency. Use
47the ID form when you only want to manage challenge records, not the zone's
48lifecycle or settings.
49
50## Add a wildcard
51
52```diff lang="typescript"
53- identifiers: ["example.com"],
54+ identifiers: ["example.com", "*.example.com"],
55```
56
57`*.example.com` covers one label such as `www.example.com`, but not the apex
58`example.com` or `a.b.example.com`. Include the apex separately when needed. Both
59names validate at `_acme-challenge.example.com`; Alchemy processes the
60authorizations sequentially and removes each challenge value afterward.
61
62## Understand record cleanup
63
64The deploy-time Cloudflare solver creates TXT records with a 60-second TTL. It
65matches both record name and value during cleanup, preserving unrelated values
66at the same name. Once `present` succeeds, cleanup runs when validation succeeds,
67fails, or is interrupted through Effect's normal finalization.
68
69A process kill or a failure after the DNS API accepted a write can prevent
70cleanup. Inspect any leftover value before removing it: another order may be
71using the same record name. Avoid concurrent issuers for the same names where
72possible.
73
74## Adjust the propagation budget
75
76```diff lang="typescript"
77 const certificate = yield* ACME.Certificate("Site", {
78 account,
79 identifiers: ["example.com", "*.example.com"],
80 solver: Cloudflare.DNS.AcmeSolver(zone),
81+ propagation: {
82+ delay: "60 seconds",
83+ timeout: "90 seconds",
84+ interval: "3 seconds",
85+ },
86 });
87```
88
89These are the deploy-time Cloudflare defaults. The timeout covers the lookup and
90post-visibility delay together. Lookup attempts are bounded, so increasing the
91timeout alone does not create unlimited polling. Keep enough budget for the
92post-visibility delay; cached previous TXT values can cause a secondary CA
93validator to reject a record even after one resolver sees it.
94
95## Choose fallback resolvers
96
97```diff lang="typescript"
98 propagation: {
99 delay: "60 seconds",
100 timeout: "90 seconds",
101 interval: "3 seconds",
102+ resolvers: [
103+ "https://cloudflare-dns.com/dns-query",
104+ "https://dns.google/resolve",
105+ ],
106 },
107```
108
109Where Node DNS APIs are available, the deploy-time check prefers authoritative
110nameservers. Otherwise it polls DNS-over-HTTPS JSON endpoints and requires every
111configured resolver to return the expected TXT value. These endpoints are the
112default fallback resolvers; private DNS needs a solver-specific visibility check.
113
114## Select the runtime solver
115
116Inside a Worker's initialization Effect:
117
118```typescript
119const dns = yield* Cloudflare.DNS.WriteDns(Zone);
120const solver = Cloudflare.DNS.acmeDnsSolver(dns);
121```
122
123The runtime helper is **`acmeDnsSolver`**, while the deploy-time helper is
124**`AcmeSolver`**. Provide `Cloudflare.DNS.WriteDnsHttp` on the host Effect. Its
125client uses a zone-scoped token created at deployment, rather than exposing the
126deployer's entire credential set to the application.
127
128## Account for runtime propagation
129
130```typescript
131const issued = yield* issuer.issue({
132 identifiers: ["api.example.com"],
133 solver,
134 propagation: { delay: "60 seconds" },
135});
136```
137
138The Cloudflare runtime solver uses a fixed delay (60 seconds by default), rather
139than authoritative or public resolver polling. For this solver, `resolvers`,
140`interval`, and `timeout` do not add a DNS visibility check. An issuance request
141can take longer than a minute; [Runtime issuance](/acme/runtime) explains the
142hosting and scheduling implications.
143
144## Implement a custom runtime solver
145
146A custom solver adapts an existing DNS client's idempotent operations:
147
148```typescript
149import type * as ACME from "alchemy/ACME";
150import type * as Effect from "effect/Effect";
151
152export const makeSolver = <R>(dns: {
153 ensureTxt: (
154 record: ACME.DnsChallengeRecord,
155 ) => Effect.Effect<void, ACME.DnsSolverError, R>;
156 removeTxt: (
157 record: ACME.DnsChallengeRecord,
158 ) => Effect.Effect<void, ACME.DnsSolverError, R>;
159}): ACME.DnsSolver<R> => ({
160 present: (record) => dns.ensureTxt(record),
161 cleanup: (record) => dns.removeTxt(record),
162});
163```
164
165Implement `ensureTxt` and `removeTxt` with your provider's DNS API, mapping its
166failures to `ACME.DnsSolverError`. Ensure must tolerate the exact value already
167existing; removal must tolerate it already being absent. Match the requested
168value, not every TXT record at that name. The default propagation check applies
169unless you supply the solver's optional `propagated` method.
170
171## Register a deploy-time solver
172
173Deploy-time props need a serializable descriptor, not a client with closures.
174For an adapter whose client has no remaining Effect requirements, register a
175factory with this shape:
176
177```typescript
178import * as ACME from "alchemy/ACME";
179import type * as Effect from "effect/Effect";
180
181export const registerExampleDns = (
182 make: (
183 descriptor: ACME.DnsSolverDescriptor,
184 ) => Effect.Effect<ACME.DnsSolver, ACME.DnsSolverError>,
185) => ACME.dnsSolverLayer("Example.DNS", make);
186```
187
188Validate the descriptor's fields inside `make`, construct the client, and merge
189the returned layer into your stack's providers. Certificate props then use
190`solver: { type: "Example.DNS", ...providerData }`. A service-backed factory can
191instead provide its required credential and HTTP layers to `dnsSolverLayer`;
192the registration captures that context for the solver. Cloudflare's implementation
193in `Cloudflare/DNS/AcmeDnsSolver.ts` is the built-in reference.
194
195## Delegated challenges
196
197The Cloudflare helper writes directly to the specified zone at the calculated
198challenge name. It does not automatically follow CNAME or NS delegations to
199another challenge zone. For delegated validation, implement a solver that
200publishes to the delegated target and verifies its visibility. Simply changing
201`zoneId` is not a delegation resolver.
202
203## Next steps
204
205- [Troubleshooting](/acme/troubleshooting) — diagnose propagation and authorization failures.
206- [Runtime issuance](/acme/runtime) — use a solver in a complete Worker.