Secrets
Your Worker needs a Stripe key. You’d rather it wasn’t in your code, your config, your shell history, or your logs. Here’s how that looks:
import { Effect, Redacted } from 'effect';
import { Secret, Worker } from '@forge-effect/infra';
export class StripeKey extends Secret.Make<StripeKey>()('stripe-key') {}
export const fetch = Effect.map(StripeKey, (key) => new Response(`a key of ${Redacted.value(key).length} characters`));
export default Worker.make('pay', { fetch }).bind(StripeKey);The declaration says the Worker has a secret called stripe-key. Its
value goes up separately, once, from your terminal:
printf %s "$VALUE" | pnpm infra secret put test/fixtures/secrets/app.ts stripe-keyThere are two kinds of secret here: a Worker’s own, as above, and one in your account’s Secrets Store, which many Workers and apps can share. This page covers both.
A Worker’s own secret
Section titled “A Worker’s own secret”Declare it
Section titled “Declare it”Secret.Make is a resource like the others: a class that is the
declaration and the client at once. Bind it, and in a handler,
yield* StripeKey is the value.
The value is a Redacted<string>, Effect’s wrapper for things that
mustn’t leak. Log it, put it in an error, JSON.stringify it: you get
<redacted:STRIPE_KEY>. You have to ask for the real thing on purpose, with
Redacted.value(key), which is easy to spot in a review.
On Cloudflare it’s the Worker secret STRIPE_KEY, the name in capitals
with underscores.
Ship, then set it
Section titled “Ship, then set it”A ship deploys the secret declared, without a value. Cloudflare keeps a Worker’s secret values across versions, so every later ship leaves the value alone. You set it after the Worker’s first ship:
printf %s "$VALUE" | pnpm infra secret put app.ts stripe-key- The value comes from standard input, never an argument, so it’s not in your shell history or the process list. At a terminal, with nothing piped in, it asks and doesn’t echo what you type.
- It’s set on each deployed Worker that binds it. Cloudflare makes a new version with it and deploys that at once.
- Add
--env pr-7to set a preview’s. - A Worker that isn’t deployed yet is refused, with what to do: ship it, then set its secret.
Find the ones you forgot
Section titled “Find the ones you forgot”A Worker whose secret has no value fails the handler that reads it, saying how to set it:
the Worker's secret STRIPE_KEY has no value: set it with `infra secret put <app.ts> stripe-key`Better to hear it before a request does. infra check --account reads
your account and lists each declared secret with no value, as
secret-missing, with the command that sets it. Before the first ship,
it’s only a note (info), since a secret can only be set once its Worker
exists. See Checks.
Locally
Section titled “Locally”infra dev reads secrets from your environment, by their binding name:
STRIPE_KEY=sk_test_123 pnpm infra dev app.tsOne that isn’t set is said when it starts:
! secret pay: STRIPE_KEY has no value: set it in the environment, by that name.
In tests
Section titled “In tests”Secret.memory provides a value:
test("a secret is a Redacted service: a handler reads it, and printing it doesn't show it", async () => { const response = await Effect.runPromise(Effect.provide(fetch, Secret.memory(StripeKey, 'sk_test_123'))); assert.equal(await response.text(), 'a key of 11 characters'); const key = await Effect.runPromise(Effect.provide(StripeKey, Secret.memory(StripeKey, 'sk_test_123'))); assert.doesNotMatch(String(key), /sk_test/); assert.doesNotMatch(JSON.stringify({ key }), /sk_test/);});The account’s Secrets Store
Section titled “The account’s Secrets Store”A Worker’s secret is that Worker’s. If five Workers across three apps all need the same key, setting it five times, and rotating it five times, gets old. Cloudflare’s Secrets Store keeps secrets for the whole account, and any Worker can bind one.
import { Effect, Redacted } from 'effect';
import { App, SecretsStore, Worker } from '@forge-effect/infra';
export class Greeting extends SecretsStore.Secret<Greeting>()('greeting-key') {}
const fetch = Effect.gen(function* () { const value = Redacted.value(yield* (yield* Greeting).get); // read from the store, Redacted const digest = yield* Effect.promise(() => crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))); return new Response([...new Uint8Array(digest)].slice(0, 4).map((b) => b.toString(16).padStart(2, '0')).join(''));}).pipe(Effect.orDie);
export const reader = Worker.make('reader', { fetch }).bind(Greeting);
export default App.make('vault', { workers: [reader] });Notice the extra step: the client’s get is an Effect, because Cloudflare
reads the value from the store when you ask, and that can fail
(StoreSecretError: it isn’t there, or this Worker may not read it).
Which secret, in which store
Section titled “Which secret, in which store”By default greeting-key is the secret greeting_key (underscores) in
your account’s only store. Either can be named:
/** `STRIPE_LIVE_KEY` in the store called `payments`. */export class StripeKey extends SecretsStore.Secret<StripeKey>()('stripe-key', { store: 'payments', // A name in the store, never a value. secret: 'STRIPE_LIVE_KEY', // pragma: allowlist secret}) {}Setting it
Section titled “Setting it”The same command as a Worker’s secret:
printf %s "$VALUE" | pnpm infra secret put test/fixtures/vault/app.ts greeting-keyIt sets the value in the store, not on a Worker. If your account has no
store yet, it makes one, called default. If it has several and the
secret names none, it stops and lists them.
What a ship checks
Section titled “What a ship checks”A ship binds the secret by its store’s id. Before it makes anything, it checks the store is there and the secret has a value; if not, it stops with nothing shipped:
stopped: the store secret greeting_key has no value; nothing was shipped: set each with `infra secret put <app.ts> <name>`, which makes the store if the account has noneThe store isn’t the app’s. It’s the account’s, shared by whoever binds
it, so no destroy or prune ever deletes it or its secrets.
In tests
Section titled “In tests”SecretsStore.memory stands in:
const body = await Effect.runPromise( Effect.flatMap(Worker.handle(reader, new Request('https://x.dev/')), (r) => Effect.promise(() => r.text())).pipe(Effect.provide(SecretsStore.memory(Greeting, 'hello'))),);// The first four bytes of SHA-256("hello").assert.equal(body, '2cf24dba');Which one?
Section titled “Which one?”Secret.Make |
SecretsStore.Secret |
|
|---|---|---|
| Belongs to | each Worker that binds it | the account |
| Set with | infra secret put, per environment |
infra secret put, once for everyone |
| Read as | yield* Key |
yield* (yield* Key).get, which can fail |
| Missing a value | check --account says so |
the ship stops |
infra dev |
from your environment | not read |
Deleted by destroy |
with its Worker | never |
Reach for a Worker’s own secret first. Use the store when several apps share one value, or when one person rotates a key for everyone.
Good to know
Section titled “Good to know”- Checked live: a Worker’s secret missing, set, read, and kept by the
next ship;
infra secret putmaking a store, setting a store secret, and the deployed Worker reading it. infra devdoesn’t read the Secrets Store, so a store secret is unwired locally: reading it fails, andinfra devsays so when it starts. Test those handlers withSecretsStore.memory.- Setting a Worker’s secret deploys a new version at once, with no stepped rollout: that’s how Cloudflare applies secrets.
- Settings, logs and Tail Workers
- Checks and diagnostics, for
check --account.