Skip to content

Part 4: Secrets and Cleanup

In this part the Service reads an API token from your .env with Config.Redacted and uses it to guard a route. Then you tear the stack down so billing stops.

Create a .env file next to alchemy.run.ts:

.env
API_TOKEN=not-a-real-token

Alchemy reads Config values from the env of whoever runs the deploy, which includes .env. Keep .env out of version control.

Yield Config.Redacted in the Service’s constructor Effect:

src/api.ts
import * as Config from "effect/Config";
// ...
Effect.gen(function* () {
const mount = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 });
const fs = yield* FileSystem.FileSystem;
const apiToken = yield* Config.Redacted("API_TOKEN");
return {
fetch: /* ... */,
};
}).pipe(Effect.provide(Fly.MountVolumeLive)),

At deploy time Alchemy reads API_TOKEN and writes it onto the Machine as a secret. At runtime the same line resolves from that env var. apiToken is Redacted<string>, so it is never logged.

Add a /secret route that answers only when the request carries the token:

src/api.ts
import * as Redacted from "effect/Redacted";
// ...
if (url.pathname === "/health") {
return HttpServerResponse.json({ ok: true });
}
if (url.pathname === "/secret") {
const expected = `Bearer ${Redacted.value(apiToken)}`;
return request.headers["authorization"] === expected
? HttpServerResponse.text("authorized")
: HttpServerResponse.text("unauthorized", { status: 401 });
}
const file = `${mount.path}${url.pathname}`;

Redacted.value unwraps the token only where the comparison needs the raw string.

Terminal window
bun alchemy deploy
Plan: 1 to update

~ Api (Fly.Service)

Proceed?
◉ Yes ○ No
✓ Api (Fly.Service) updated
Terminal window
curl https://myapp-api-dev-a1b2c3d4.fly.dev/secret
# → unauthorized
curl -H "Authorization: Bearer not-a-real-token" \
https://myapp-api-dev-a1b2c3d4.fly.dev/secret
# → authorized

You’re done — tear everything down so Machine and Volume billing stops:

Terminal window
bun alchemy destroy

Deleting the Service deletes its Machine, its Volume, and the App it created, along with the App’s addresses.

Over four parts you built a complete Fly deployment:

  • An HTTP Service in its own App with a generated globally unique name and a public fly.dev URL
  • Machines updated only when the code hash changes
  • A Volume mounted at /data whose contents outlive deploys
  • A secret from .env readable from the Service, never logged
  • Services — private Services, background workers, and grouping Services in one App.
  • IPs & certificates — public and private Services, and your own hostname.
  • Machines — raw images without a bundle.
  • Volumes — snapshots, extend, region rules.
  • Secrets — Fly.Secret, GetSecret / ListSecrets / WriteSecret, plus KMS SecretKey with Encrypt / Decrypt / Sign / Verify.
  • Testing — deploy this stack from an integration test and drive it over HTTP.
  • CI — run alchemy deploy from GitHub Actions with FLY_API_TOKEN.