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 init, 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: at deploy time it can mint an App
deploy token for the Machine, and at runtime it hands you a
get() for 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}`;secrets.get() defaults to 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.