Part 4: Secrets and Cleanup
A secret only this Service needs from .env is
Config.Redacted. See Secrets.
This part is for a value Fly should own: an App Secret Fly
injects into every Machine. You will read it with GetSecret, then
tear the stack down so billing stops.
Declare a Secret
Section titled “Declare a Secret”Add a Secret next to the App. Wrap the value with Redacted.make so
it is never logged:
import * as Fly from "alchemy/Fly";import * as Redacted from "effect/Redacted";
export const Site = Fly.App("Site");
export const PublicIp = Fly.IpAssignment("Shared", { /* ... */ });
export const ApiToken = Fly.Secret("ApiToken", { app: Site, name: "API_TOKEN", value: Redacted.make("not-a-real-token"),});name is the env-var Fly injects into Machines (API_TOKEN). Omit
it and Alchemy generates an ownership-stamped name instead.
Bind GetSecret
Section titled “Bind GetSecret”Inside the Service’s constructor, bind the Secret — pass the declaration directly, no yielding required:
import { Site } from "./app.ts";import { ApiToken, Site } from "./app.ts";
Effect.gen(function* () { const mount = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 }); const fs = yield* FileSystem.FileSystem; const get = yield* Fly.GetSecret(ApiToken);
return { fetch: /* ... */, }; }).pipe(Effect.provide(Fly.MountVolumeLive)),GetSecret is a binding: Alchemy transports the App name, secret
name, and deployment’s org token to the Machine through resource
Outputs. At runtime, get() reads this Secret. Listing every secret
on the App is ListSecrets(Site) — Fly’s list API is app-scoped.
Provide the binding layer
Section titled “Provide the binding layer”Bindings declare a capability; layers implement it. Provide
GetSecretHttp next to MountVolumeLive:
}).pipe( Effect.provide(Fly.MountVolumeLive), Effect.provide(Fly.GetSecretHttp), ),Serve the secret’s name
Section titled “Serve the secret’s name”Add a /secret route that returns the secret’s name, never the
plaintext:
if (url.pathname === "/health") { return HttpServerResponse.json({ ok: true });}if (url.pathname === "/secret") { const got = yield* get().pipe(Effect.orDie); return HttpServerResponse.json({ name: got.name });}const file = `${mount.path}${url.pathname}`;get() reads the bound Secret. Fly only returns
plaintext from a Machine in the same App; from an Action you get
metadata.
Yield the Secret from the Stack
Section titled “Yield the Secret from the Stack”import { PublicIp, Site } from "./src/app.ts";import { ApiToken, PublicIp, Site } from "./src/app.ts";
Effect.gen(function* () { const site = yield* Site; yield* PublicIp; yield* ApiToken; const api = yield* Api;Deploy
Section titled “Deploy”bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployPlan: 1 to create, 1 to update + ApiToken (Fly.Secret) ~ Api (Fly.Service) Proceed? ◉ Yes ○ No ✓ ApiToken (Fly.Secret) created ✓ Api (Fly.Service) updated
Try it out
Section titled “Try it out”curl https://myapp-site-dev-a1b2c3d4.fly.dev/secret# → {"name":"API_TOKEN"}Clean up
Section titled “Clean up”You’re done — tear everything down so Machine and Volume billing stops:
bun alchemy destroynpm run alchemy destroypnpm alchemy destroyyarn alchemy destroyAlchemy deletes everything in reverse dependency order — Service, Secret, Volume, IP, App.
Over four parts you built a complete Fly deployment:
- An App with a generated globally unique name
- An HTTP Service bundled into a Machine, updated only when its code hash changes
- A Volume mounted at
/datawhose contents outlive deploys - An App Secret readable from the Service, never logged
Where next
Section titled “Where next”- Services — background workers, env, multiple Services per App.
- Machines — raw images without a bundle.
- Volumes — snapshots, extend, region rules.
- Secrets —
GetSecret/ListSecrets/WriteSecret, plus KMSSecretKeywithEncrypt/Decrypt/Sign/Verify. - Testing — deploy this stack from an integration test and drive it over HTTP.
- CI — run
alchemy deployfrom GitHub Actions withFLY_API_TOKEN.