| 1 | --- |
| 2 | title: Secrets |
| 3 | description: Config.Redacted on a Service, Fly.Secret when Fly should own the value, and KMS keys. |
| 4 | --- |
| 5 | |
| 6 | Most secrets in a [`Service`](/fly/compute/services) come from your |
| 7 | `.env`. Yield `Config.Redacted` in the constructor. Alchemy binds the value |
| 8 | onto the Machine. |
| 9 | |
| 10 | Use [`Fly.Secret`](/providers/fly/secret) when the value should be |
| 11 | shared across Machines and managed in one place by Fly. Use |
| 12 | [`SecretKey`](/providers/fly/secretkey) for Fly KMS. |
| 13 | |
| 14 | ## Bind from `.env` |
| 15 | |
| 16 | ```typescript |
| 17 | import * as Config from "effect/Config"; |
| 18 | import * as Redacted from "effect/Redacted"; |
| 19 | |
| 20 | export default class Api extends Fly.Service<Api>()( |
| 21 | "Api", |
| 22 | { main: import.meta.url, port: 3000 }, |
| 23 | Effect.gen(function* () { |
| 24 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 25 | |
| 26 | return { |
| 27 | fetch: Effect.gen(function* () { |
| 28 | const token = Redacted.value(apiKey); |
| 29 | // ... |
| 30 | }), |
| 31 | }; |
| 32 | }), |
| 33 | ) {} |
| 34 | ``` |
| 35 | |
| 36 | `Config.Redacted("API_KEY")` reads `API_KEY` from the env of whoever |
| 37 | runs the deploy and writes it onto the Machine. At runtime the same |
| 38 | line resolves from that env var. |
| 39 | |
| 40 | The value is `Redacted<string>`. Unwrap with `Redacted.value` only |
| 41 | where you need the raw string. Do not pass `env: { ... }` on a |
| 42 | Service. Yield Config. |
| 43 | |
| 44 | See [Secrets & Config](/environments/secrets) for combinators and |
| 45 | stages. |
| 46 | |
| 47 | ## When Fly should own the secret |
| 48 | |
| 49 | `Fly.Secret` is the App vault. Fly injects it as an environment |
| 50 | variable on every Machine in a [`Fly.App`](/fly/compute/apps). Use it |
| 51 | when the value is shared and managed in one place, not when only this |
| 52 | Service reads a deploy-time `.env` key. |
| 53 | |
| 54 | ```typescript |
| 55 | import * as Fly from "alchemy/Fly"; |
| 56 | import * as Redacted from "effect/Redacted"; |
| 57 | |
| 58 | export const Site = Fly.App("Site"); |
| 59 | |
| 60 | export const ApiToken = Fly.Secret("ApiToken", { |
| 61 | app: Site, |
| 62 | name: "API_TOKEN", |
| 63 | value: Redacted.make("…"), |
| 64 | }); |
| 65 | ``` |
| 66 | |
| 67 | A `Fly.Secret` targets a `Fly.App`, not a Service's own App. Run the |
| 68 | Services that read it in that App with `app: Site` — see |
| 69 | [Group Services in one App](/fly/compute/services#group-services-in-one-app). |
| 70 | |
| 71 | `name` is the env-var Machines see. Omit it and Alchemy generates an |
| 72 | ownership-stamped name. Updating `value` is in place. Changing `app` |
| 73 | or `name` replaces. |
| 74 | |
| 75 | The resolved resource exposes `name`, `digest`, `createdAt`, and |
| 76 | `updatedAt`. Never the plaintext. |
| 77 | |
| 78 | ## Get, list, and write |
| 79 | |
| 80 | `GetSecret` is bound to one Secret. `ListSecrets` is bound to the |
| 81 | App. The Service runs in the Secret's App: |
| 82 | |
| 83 | ```typescript |
| 84 | export default class Api extends Fly.Service<Api>()( |
| 85 | "Api", |
| 86 | { app: Site, main: import.meta.url, port: 3000 }, |
| 87 | Effect.gen(function* () { |
| 88 | const get = yield* Fly.GetSecret(ApiToken); |
| 89 | |
| 90 | return { |
| 91 | fetch: Effect.gen(function* () { |
| 92 | const got = yield* get().pipe(Effect.orDie); |
| 93 | return HttpServerResponse.json({ name: got.name }); |
| 94 | }), |
| 95 | }; |
| 96 | }).pipe(Effect.provide(Fly.GetSecretHttp)), |
| 97 | ) {} |
| 98 | ``` |
| 99 | |
| 100 | ```typescript |
| 101 | const list = yield* Fly.ListSecrets(Site); |
| 102 | const { secrets } = yield* list(); |
| 103 | ``` |
| 104 | |
| 105 | From an Action, the org token can list any App in the org. From a |
| 106 | Machine, deploy tokens are per-App. Mixing Apps on one Machine |
| 107 | shares one `FLY_API_TOKEN` and is not supported. |
| 108 | |
| 109 | Fly only returns plaintext from a Machine in the same App. From |
| 110 | deploy-time Actions you get metadata (name, digest, timestamps). |
| 111 | |
| 112 | Write from an Action with `WriteSecret`: |
| 113 | |
| 114 | ```typescript |
| 115 | const Seed = Alchemy.Action( |
| 116 | "Seed", |
| 117 | Effect.gen(function* () { |
| 118 | const secrets = yield* Fly.WriteSecret(ApiToken); |
| 119 | |
| 120 | return Effect.fn(function* () { |
| 121 | yield* secrets.update("API_TOKEN", Redacted.make("sk_live_rotated")); |
| 122 | }); |
| 123 | }).pipe(Effect.provide(Fly.WriteSecretHttp)), |
| 124 | ); |
| 125 | ``` |
| 126 | |
| 127 | Inside a Service, Alchemy mints an App deploy token for the Machine. |
| 128 | Inside an Action, the ambient `FLY_API_TOKEN` is used instead. |
| 129 | |
| 130 | ## KMS keys |
| 131 | |
| 132 | [`SecretKey`](/providers/fly/secretkey) is an App KMS key, not an |
| 133 | env secret. Generate a random key or set raw material. Private |
| 134 | bytes never appear in attributes. Like `Fly.Secret`, it targets a |
| 135 | `Fly.App`; bind it from a Service that runs in that App (`app: Site`). |
| 136 | |
| 137 | ```typescript |
| 138 | export const Signing = Fly.SecretKey("Signing", { |
| 139 | app: Site, |
| 140 | type: "nacl_sign", |
| 141 | }); |
| 142 | |
| 143 | export const Box = Fly.SecretKey("Box", { |
| 144 | app: Site, |
| 145 | type: "nacl_secretbox", |
| 146 | }); |
| 147 | ``` |
| 148 | |
| 149 | `type` is Fly's key type (`nacl_sign`, `nacl_box`, `nacl_secretbox`, |
| 150 | `hs256`, `es256`, `xaes256gcm`, …). Changing `app`, `name`, or |
| 151 | `type` replaces the key. |
| 152 | |
| 153 | ## Encrypt, Decrypt, Sign, Verify |
| 154 | |
| 155 | One binding per operation. The key is fixed when you bind. |
| 156 | |
| 157 | ```typescript |
| 158 | const encrypt = yield* Fly.Encrypt(Box); |
| 159 | const decrypt = yield* Fly.Decrypt(Box); |
| 160 | |
| 161 | const { ciphertext } = yield* encrypt({ plaintext: bytes }); |
| 162 | const { plaintext } = yield* decrypt({ ciphertext }); |
| 163 | const raw = Redacted.value(plaintext); |
| 164 | ``` |
| 165 | |
| 166 | ```typescript |
| 167 | const sign = yield* Fly.Sign(Signing); |
| 168 | const verify = yield* Fly.Verify(Signing); |
| 169 | |
| 170 | const { signature } = yield* sign({ plaintext: bytes }); |
| 171 | yield* verify({ plaintext: bytes, signature }); |
| 172 | ``` |
| 173 | |
| 174 | Provide the matching `*Http` layer on the Service or Action. |
| 175 | Generate, set, and delete stay on the `SecretKey` resource. |
| 176 | |
| 177 | Decrypt wraps plaintext in `Redacted`. Fly crypto ops need a KMS |
| 178 | token. Org API tokens are typed `Forbidden`. Encrypt and sign from |
| 179 | a Service, not a laptop Action. |
| 180 | |
| 181 | ## Where next |
| 182 | |
| 183 | The [tutorial](/fly/tutorial/part-4) reads a `Config.Redacted` secret |
| 184 | from a Service. [Services](/fly/compute/services) covers |
| 185 | `Config.Redacted` and grouping Services in one App. [Postgres](/fly/data/postgres), |
| 186 | [Redis](/fly/data/redis), and [Tigris](/fly/data/tigris) bind |
| 187 | typed clients (`ConnectPostgres`, `ReadWriteRedis`, `PutObject`) |
| 188 | that use Fly-owned secrets internally. See the |
| 189 | [`Secret`](/providers/fly/secret), |
| 190 | [`GetSecret`](/providers/fly/getsecret), |
| 191 | [`ListSecrets`](/providers/fly/listsecrets), |
| 192 | [`SecretKey`](/providers/fly/secretkey), |
| 193 | [`Encrypt`](/providers/fly/encrypt), |
| 194 | [`Decrypt`](/providers/fly/decrypt), |
| 195 | [`Sign`](/providers/fly/sign), and |
| 196 | [`Verify`](/providers/fly/verify) references. |