1---
2title: Getting started
3description: Issue a staging TLS certificate with Let's Encrypt and an existing Cloudflare DNS zone.
4---
5
6This guide issues a certificate for a domain you control. It uses Let's Encrypt
7staging, so you can validate the deployment without consuming production issuance
8limits. The resulting certificate is deliberately **not trusted by browsers**.
9
10## Prepare the project
11
12Install Alchemy and the matching Effect version using [Cloudflare setup](/cloudflare/setup).
13You need an active Cloudflare DNS zone, its zone ID, and credentials that can read,
14create, and delete DNS records in that zone. The domain's authoritative nameservers
15must already point to Cloudflare.
16
17Use a domain you own wherever this guide says `example.com`. Set
18`CLOUDFLARE_ZONE_ID` in your project's `.env` to the zone ID from its Cloudflare
19dashboard. This example references the existing zone by ID; it does not create,
20adopt, modify the settings of, or delete the zone itself.
21
22## Create the stack
23
24Create `alchemy.run.ts`:
25
26```typescript
27import * as Alchemy from "alchemy";
28import * as Effect from "effect/Effect";
29import * as Layer from "effect/Layer";
30
31export default Alchemy.Stack(
32 "Certificates",
33 { providers: Layer.empty, state: Alchemy.localState() },
34 Effect.gen(function* () {
35 return {};
36 }),
37);
38```
39
40The stack groups the certificate resources and persists their state locally.
41Treat that state as secret material and keep it out of source control. For a
42shared deployment runner, use a protected [remote state store](/state-store).
43
44## Register the providers
45
46```diff lang="typescript"
47+import * as ACME from "alchemy/ACME";
48+import * as Cloudflare from "alchemy/Cloudflare";
49
50 export default Alchemy.Stack(
51 "Certificates",
52- { providers: Layer.empty, state: Alchemy.localState() },
53+ {
54+ providers: Layer.mergeAll(ACME.providers(), Cloudflare.providers()),
55+ state: Alchemy.localState(),
56+ },
57```
58
59`ACME.providers()` manages accounts and certificates. `Cloudflare.providers()`
60registers the Cloudflare DNS solver used to prove control of the domain.
61
62## Connect Cloudflare
63
64```sh
65alchemy profile edit --add Cloudflare
66```
67
68Select the Cloudflare account containing your zone. You can use OAuth or a
69suitably scoped API token; [Cloudflare setup](/cloudflare/setup) also documents
70non-interactive environment credentials for CI.
71
72## Create an ACME account
73
74Inside the stack's `Effect.gen`, add the account before `return`:
75
76```diff lang="typescript"
77 Effect.gen(function* () {
78+ const account = yield* ACME.Account("Issuer", {
79+ ca: ACME.LetsEncryptStaging,
80+ contact: ["mailto:ops@example.com"],
81+ termsOfServiceAgreed: true,
82+ });
83 return {};
84 }),
85```
86
87Replace the contact address with yours and review the CA's terms before agreeing.
88The account gets its own signing key, which Alchemy persists in stack state and
89reuses on subsequent deployments.
90
91## Request the certificate
92
93```diff lang="typescript"
94+import * as Config from "effect/Config";
95
96 const account = yield* ACME.Account("Issuer", {
97 ca: ACME.LetsEncryptStaging,
98 contact: ["mailto:ops@example.com"],
99 termsOfServiceAgreed: true,
100 });
101+ const certificate = yield* ACME.Certificate("Site", {
102+ account,
103+ identifiers: ["example.com"],
104+ solver: Cloudflare.DNS.AcmeSolver({
105+ zoneId: Config.String("CLOUDFLARE_ZONE_ID"),
106+ }),
107+ });
108```
109
110The solver publishes a temporary `_acme-challenge.example.com` TXT record.
111It waits for propagation before asking the CA to validate it, then removes that
112challenge record. Existing unrelated TXT values are preserved.
113
114## Return certificate metadata
115
116```diff lang="typescript"
117- return {};
118+ return {
119+ serial: certificate.serial,
120+ expires: certificate.notAfter,
121+ };
122```
123
124Return metadata rather than private keys. The PEM chain and redacted key remain
125available as resource outputs for a [TLS consumer](/acme/using-certificates).
126
127## Deploy to a development stage
128
129```sh
130alchemy deploy --stage dev
131```
132
133Allow time for DNS validation: Cloudflare's solver includes a 60-second wait to
134let cached challenge values expire. A successful deployment returns the serial
135number and expiry. It does not configure a website or an HTTPS listener.
136
137## Review the complete file
138
139The steps above produce this `alchemy.run.ts`:
140
141```typescript
142import * as Alchemy from "alchemy";
143import * as ACME from "alchemy/ACME";
144import * as Cloudflare from "alchemy/Cloudflare";
145import * as Config from "effect/Config";
146import * as Effect from "effect/Effect";
147import * as Layer from "effect/Layer";
148
149export default Alchemy.Stack(
150 "Certificates",
151 {
152 providers: Layer.mergeAll(ACME.providers(), Cloudflare.providers()),
153 state: Alchemy.localState(),
154 },
155 Effect.gen(function* () {
156 const account = yield* ACME.Account("Issuer", {
157 ca: ACME.LetsEncryptStaging,
158 contact: ["mailto:ops@example.com"],
159 termsOfServiceAgreed: true,
160 });
161 const certificate = yield* ACME.Certificate("Site", {
162 account,
163 identifiers: ["example.com"],
164 solver: Cloudflare.DNS.AcmeSolver({
165 zoneId: Config.String("CLOUDFLARE_ZONE_ID"),
166 }),
167 });
168 return {
169 serial: certificate.serial,
170 expires: certificate.notAfter,
171 };
172 }),
173);
174```
175
176## Clean up the development stack
177
178```sh
179alchemy destroy --stage dev
180```
181
182Destruction deactivates the ACME account and removes the stack's certificate
183state. By default it does not revoke an issued certificate. The existing DNS
184zone is not a managed resource in this example and remains intact. Read
185[Renewal & revocation](/acme/renewal) before choosing a production deletion policy.
186
187## Next steps
188
189- [Certificate authorities](/acme/certificate-authorities) — switch to a trusted production CA.
190- [DNS validation](/acme/dns-validation) — add a wildcard or customize propagation.
191- [Using certificates](/acme/using-certificates) — attach the chain and key to a TLS consumer.