| 1 | --- |
| 2 | title: "Part 4: Secrets and Cleanup" |
| 3 | description: Read a secret from .env in the Service with Config.Redacted, guard a route with it, and destroy the stack. |
| 4 | sidebar: |
| 5 | order: 4 |
| 6 | --- |
| 7 | |
| 8 | import Terminal from "../../../../components/Terminal.astro"; |
| 9 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 10 | |
| 11 | In this part the Service reads an API token from your `.env` with |
| 12 | `Config.Redacted` and uses it to guard a route. Then you tear the |
| 13 | stack down so billing stops. |
| 14 | |
| 15 | ## Add the secret to `.env` |
| 16 | |
| 17 | Create a `.env` file next to `alchemy.run.ts`: |
| 18 | |
| 19 | ```sh |
| 20 | # .env |
| 21 | API_TOKEN=not-a-real-token |
| 22 | ``` |
| 23 | |
| 24 | Alchemy reads Config values from the env of whoever runs the deploy, |
| 25 | which includes `.env`. Keep `.env` out of version control. |
| 26 | |
| 27 | ## Read it in the constructor |
| 28 | |
| 29 | Yield `Config.Redacted` in the Service's constructor Effect: |
| 30 | |
| 31 | ```diff lang="typescript" |
| 32 | // src/api.ts |
| 33 | +import * as Config from "effect/Config"; |
| 34 | // ... |
| 35 | Effect.gen(function* () { |
| 36 | const mount = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 }); |
| 37 | const fs = yield* FileSystem.FileSystem; |
| 38 | + const apiToken = yield* Config.Redacted("API_TOKEN"); |
| 39 | |
| 40 | return { |
| 41 | fetch: /* ... */, |
| 42 | }; |
| 43 | }).pipe(Effect.provide(Fly.MountVolumeLive)), |
| 44 | ``` |
| 45 | |
| 46 | At deploy time Alchemy reads `API_TOKEN` and writes it onto the |
| 47 | Machine as a secret. At runtime the same line resolves from that env |
| 48 | var. `apiToken` is `Redacted<string>`, so it is never logged. |
| 49 | |
| 50 | ## Guard a route with the token |
| 51 | |
| 52 | Add a `/secret` route that answers only when the request carries the |
| 53 | token: |
| 54 | |
| 55 | ```diff lang="typescript" |
| 56 | // src/api.ts |
| 57 | +import * as Redacted from "effect/Redacted"; |
| 58 | // ... |
| 59 | if (url.pathname === "/health") { |
| 60 | return HttpServerResponse.json({ ok: true }); |
| 61 | } |
| 62 | +if (url.pathname === "/secret") { |
| 63 | + const expected = `Bearer ${Redacted.value(apiToken)}`; |
| 64 | + return request.headers["authorization"] === expected |
| 65 | + ? HttpServerResponse.text("authorized") |
| 66 | + : HttpServerResponse.text("unauthorized", { status: 401 }); |
| 67 | +} |
| 68 | const file = `${mount.path}${url.pathname}`; |
| 69 | ``` |
| 70 | |
| 71 | `Redacted.value` unwraps the token only where the comparison needs |
| 72 | the raw string. |
| 73 | |
| 74 | ## Deploy |
| 75 | |
| 76 | <Tabs syncKey="pkgManager"> |
| 77 | <TabItem label="bun" icon="bun"> |
| 78 | <Code code="bun alchemy deploy" lang="sh" /> |
| 79 | </TabItem> |
| 80 | <TabItem label="npm" icon="npm"> |
| 81 | <Code code="npm run alchemy deploy" lang="sh" /> |
| 82 | </TabItem> |
| 83 | <TabItem label="pnpm" icon="pnpm"> |
| 84 | <Code code="pnpm alchemy deploy" lang="sh" /> |
| 85 | </TabItem> |
| 86 | <TabItem label="yarn" icon="seti:yarn"> |
| 87 | <Code code="yarn alchemy deploy" lang="sh" /> |
| 88 | </TabItem> |
| 89 | </Tabs> |
| 90 | |
| 91 | <Terminal content={`[u]Plan[/u]: [y]1 to update[/y] |
| 92 | |
| 93 | [y]~[/y] [b]Api[/b] [d](Fly.Service)[/d] |
| 94 | |
| 95 | Proceed? |
| 96 | ◉ Yes ○ No |
| 97 | [g]✓[/g] [b]Api[/b] [d](Fly.Service)[/d] [y]updated[/y]`} /> |
| 98 | |
| 99 | ## Try it out |
| 100 | |
| 101 | ```sh |
| 102 | curl https://myapp-api-dev-a1b2c3d4.fly.dev/secret |
| 103 | # → unauthorized |
| 104 | |
| 105 | curl -H "Authorization: Bearer not-a-real-token" \ |
| 106 | https://myapp-api-dev-a1b2c3d4.fly.dev/secret |
| 107 | # → authorized |
| 108 | ``` |
| 109 | |
| 110 | :::tip[Sharing a secret or adding your own domain?] |
| 111 | A value several Services share belongs in a |
| 112 | [`Fly.Secret`](/fly/data/secrets#when-fly-should-own-the-secret), and |
| 113 | a custom domain needs a |
| 114 | [`Fly.Certificate`](/fly/networking#use-your-own-hostname). Both |
| 115 | attach to a `Fly.App`, so the Services that use them join that App |
| 116 | with [`app`](/fly/compute/services#group-services-in-one-app). |
| 117 | ::: |
| 118 | |
| 119 | ## Clean up |
| 120 | |
| 121 | You're done — tear everything down so Machine and Volume billing |
| 122 | stops: |
| 123 | |
| 124 | <Tabs syncKey="pkgManager"> |
| 125 | <TabItem label="bun" icon="bun"> |
| 126 | <Code code="bun alchemy destroy" lang="sh" /> |
| 127 | </TabItem> |
| 128 | <TabItem label="npm" icon="npm"> |
| 129 | <Code code="npm run alchemy destroy" lang="sh" /> |
| 130 | </TabItem> |
| 131 | <TabItem label="pnpm" icon="pnpm"> |
| 132 | <Code code="pnpm alchemy destroy" lang="sh" /> |
| 133 | </TabItem> |
| 134 | <TabItem label="yarn" icon="seti:yarn"> |
| 135 | <Code code="yarn alchemy destroy" lang="sh" /> |
| 136 | </TabItem> |
| 137 | </Tabs> |
| 138 | |
| 139 | Deleting the Service deletes its Machine, its Volume, and the App it |
| 140 | created, along with the App's addresses. |
| 141 | |
| 142 | ## Recap |
| 143 | |
| 144 | Over four parts you built a complete Fly deployment: |
| 145 | |
| 146 | - An HTTP Service in its own App with a generated globally unique |
| 147 | name and a public `fly.dev` URL |
| 148 | - Machines updated only when the code hash changes |
| 149 | - A Volume mounted at `/data` whose contents outlive deploys |
| 150 | - A secret from `.env` readable from the Service, never logged |
| 151 | |
| 152 | ## Where next |
| 153 | |
| 154 | - [Services](/fly/compute/services) — private Services, background |
| 155 | workers, and grouping Services in one App. |
| 156 | - [IPs & certificates](/fly/networking) — public and private |
| 157 | Services, and your own hostname. |
| 158 | - [Machines](/fly/compute/machines) — raw images without a bundle. |
| 159 | - [Volumes](/fly/data/volumes) — snapshots, extend, region rules. |
| 160 | - [Secrets](/fly/data/secrets) — `Fly.Secret`, `GetSecret` / |
| 161 | `ListSecrets` / `WriteSecret`, plus KMS `SecretKey` with |
| 162 | `Encrypt` / `Decrypt` / `Sign` / `Verify`. |
| 163 | - [Testing](/testing) — deploy this stack from an integration test |
| 164 | and drive it over HTTP. |
| 165 | - [CI](/environments/ci) — run `alchemy deploy` from GitHub Actions |
| 166 | with `FLY_API_TOKEN`. |