1---
2title: Using certificates
3description: Upload an ACME certificate to Fly or pass its chain and private key to another TLS consumer.
4---
5
6Issuance creates certificate material; it does not create an HTTPS endpoint.
7A TLS consumer must receive the matching chain and private key, present the
8certificate for the correct hostname, and reload replacements after renewal.
9
10## Choose the right output
11
12| Output | Contents | Typical use |
13| --- | --- | --- |
14| `certificate` | Leaf certificate PEM | Revocation or certificate inspection |
15| `chain` | Full PEM chain, leaf first | TLS server or certificate upload |
16| `privateKey` | Redacted PKCS#8 private key PEM | TLS server or certificate upload |
17| `notAfter` | ISO 8601 expiry | Monitoring and renewal scheduling |
18| `serial` | Certificate serial number | Identifying the installed version |
19
20The account's `privateKey` is a different key, used to sign ACME requests. A TLS
21consumer needs the **certificate's** private key, never the account's key.
22
23## Register Fly's provider
24
25Start with the [Getting started](/acme/getting-started) stack and add Fly:
26
27```diff lang="typescript"
28+import * as Fly from "alchemy/Fly";
29
30- providers: Layer.mergeAll(ACME.providers(), Cloudflare.providers()),
31+ providers: Layer.mergeAll(
32+ ACME.providers(),
33+ Cloudflare.providers(),
34+ Fly.providers(),
35+ ),
36```
37
38Connect your Fly account using [Fly setup](/fly/setup). The ACME account and DNS
39solver remain independent of Fly; only the upload resource uses Fly credentials.
40
41## Create the destination App
42
43Inside the stack Effect, before the upload:
44
45```typescript
46const app = yield* Fly.App("Site", {});
47```
48
49Use your application's existing `App` instead when it already owns the TLS
50endpoint. Creating an App alone does not start a service or allocate public IPs.
51
52## Upload the certificate
53
54After resolving the ACME certificate:
55
56```typescript
57yield* Fly.Certificate("SiteTls", {
58 app,
59 hostname: "example.com",
60 kind: "custom",
61 fullchain: certificate.chain,
62 privateKey: certificate.privateKey,
63});
64```
65
66The resource outputs establish a dependency on issuance. After an ACME renewal
67changes the chain and key, the same deployment updates the Fly upload. Pass the
68redacted key directly; the provider handles unwrapping it for the API.
69
70## Review the complete upload stack
71
72This example uses Let's Encrypt **production** because the destination is a
73public TLS consumer. First verify DNS issuance with the staging-only getting
74started example. Replace the hostname, contact address, and zone configuration
75with your own values:
76
77```typescript
78import * as Alchemy from "alchemy";
79import * as ACME from "alchemy/ACME";
80import * as Cloudflare from "alchemy/Cloudflare";
81import * as Fly from "alchemy/Fly";
82import * as Config from "effect/Config";
83import * as Effect from "effect/Effect";
84import * as Layer from "effect/Layer";
85
86export default Alchemy.Stack(
87 "SiteCertificates",
88 {
89 providers: Layer.mergeAll(
90 ACME.providers(),
91 Cloudflare.providers(),
92 Fly.providers(),
93 ),
94 state: Alchemy.localState(),
95 },
96 Effect.gen(function* () {
97 const app = yield* Fly.App("Site", {});
98 const account = yield* ACME.Account("Issuer", {
99 ca: ACME.LetsEncrypt,
100 contact: ["mailto:ops@example.com"],
101 termsOfServiceAgreed: true,
102 });
103 const certificate = yield* ACME.Certificate("Site", {
104 account,
105 identifiers: ["example.com"],
106 solver: Cloudflare.DNS.AcmeSolver({
107 zoneId: Config.String("CLOUDFLARE_ZONE_ID"),
108 }),
109 });
110 yield* Fly.Certificate("SiteTls", {
111 app,
112 hostname: "example.com",
113 kind: "custom",
114 fullchain: certificate.chain,
115 privateKey: certificate.privateKey,
116 });
117 return { appName: app.appName, expires: certificate.notAfter };
118 }),
119);
120```
121
122## Make the hostname reachable
123
124Follow [Fly IPs & certificates](/fly/networking) to publish a Service, allocate
125public IPs, and point the hostname's DNS records at the App. A successful
126certificate upload is separate from traffic routing. Check the certificate
127served by `https://example.com` after the routing is ready.
128
129For ordinary Fly-managed certificates, `Fly.Certificate` with `kind: "acme"`
130lets Fly handle issuance. Use this independent `ACME.Certificate` plus
131`kind: "custom"` combination when you need control over the issuing CA or DNS
132validation and will operate renewal yourself.
133
134## Pass outputs to another resource
135
136Alchemy resource props accept outputs directly. If another provider accepts PEM
137chain and key inputs, pass `certificate.chain` and `certificate.privateKey` to
138those fields just as in the Fly upload. Follow that provider's expected chain
139format and supported key algorithms.
140
141For a consumer outside Alchemy, build an explicit secure delivery process.
142Store the chain and key as a matching pair, restrict key access, reload the TLS
143service after renewal, and verify the certificate it serves. Exporting a PEM
144once does not subscribe the consumer to later resource updates.
145
146## Upload from an application
147
148In an initialized Fly Service or Worker, bind the destination App separately
149from the ACME account:
150
151```typescript
152const certificates = yield* Fly.WriteCertificates(Site);
153```
154
155Provide `Fly.WriteCertificatesHttp` on the host Effect. `Site` is the same App
156resource your service uses; the binding transports the necessary app identity
157and scoped credentials to runtime.
158
159## Install a runtime-issued certificate
160
161After `issuer.issue` returns inside a request or job:
162
163```typescript
164yield* certificates.upload({
165 hostname: "example.com",
166 fullchain: issued.chain,
167 privateKey: issued.privateKey,
168});
169```
170
171Use the [runtime issuance guide](/acme/runtime) for the account and DNS bindings.
172The runtime upload is not a `Fly.Certificate` stack resource. Your application
173must track and renew it; avoid having a deploy-time resource and a runtime job
174both manage the same App/hostname.
175
176## Keep removal and revocation separate
177
178Removing a Fly certificate stops that App from using it but does not revoke it
179at the issuing CA. Revoking at the CA does not remove the Fly upload. For a
180runtime-managed certificate, replace or remove the consumer copy first, then
181call `issuer.revoke` according to your retirement policy.
182
183See [Renewal & revocation](/acme/renewal), the
184[Fly Certificate reference](/providers/fly/certificate), and
185[WriteCertificates reference](/providers/fly/writecertificates) for the separate
186lifecycle controls.