t3-code-android-nightly/.repos/alchemy-effect/examples/gcp-secrets-kms
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00
..
src chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170) 2026-10-05 13:22:30 -07:00
test chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170) 2026-10-05 13:22:30 -07:00
alchemy.run.ts chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170) 2026-10-05 13:22:30 -07:00
package.json chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170) 2026-10-05 13:22:30 -07:00
README.md chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170) 2026-10-05 13:22:30 -07:00
tsconfig.json chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170) 2026-10-05 13:22:30 -07:00

gcp-secrets-kms

Encryption as a service on Cloud Run. Callers send plaintext and get back Cloud KMS ciphertext (and the reverse) without ever holding key material. Both routes are gated by an API key stored in Secret Manager.

Route Body Returns
POST /encrypt { plaintext } { ciphertext }
POST /decrypt { ciphertext } { plaintext }

Requests without the right x-api-key header get 401. Until a secret version exists the API answers 503 instead of failing open.

Architecture

  • GCP.KMS.KeyRing Keys — container for the key, in the stack's region.
  • GCP.KMS.CryptoKey DataKey — symmetric ENCRYPT_DECRYPT key.
  • GCP.SecretManager.Secret ApiKey — the API key callers must present.
  • GCP.Function Api — public Cloud Run service (invokerIamDisabled: true) serving both routes.

Bindings

Each binding grants one role to the service's runtime service account, on the single resource it was bound to:

Binding Role On
GCP.KMS.Encrypt(dataKey) roles/cloudkms.cryptoKeyEncrypter DataKey
GCP.KMS.Decrypt(dataKey) roles/cloudkms.cryptoKeyDecrypter DataKey
GCP.SecretManager.ReadSecret(apiKey) roles/secretmanager.secretAccessor ApiKey

The service reads the latest secret version on every request (and compares it in constant time), so rotating the key is a new secret version — no redeploy.

Deploy

Credentials come from your alchemy profile: run alchemy profile once and pick GCP (Service account JSON for a key file, or Stored for an access token or key kept in ~/.alchemy/credentials, plus a default region), then deploy with --profile <name>.

pnpm deploy --profile <name>

Then add the API key as a secret version (the stack prints secretId):

printf 'my-key' | gcloud secrets versions add "$SECRET_ID" --data-file=-
curl -X POST "$URL/encrypt" -H 'x-api-key: my-key' \
  -H 'content-type: application/json' -d '{"plaintext":"hello"}'

Test

ALCHEMY_PROFILE=<name> bun test

Deploys the stack, seeds the secret, checks the 401s, round-trips a payload through /encrypt and /decrypt, decrypts the ciphertext directly with the KMS API, then destroys the stack and checks that the key was released and the secret, service, and image repository are gone. Needs Docker (the service image is built locally).

Destroy

pnpm destroy --profile <name>

Cloud KMS never deletes key rings, and keeps keys for at least 24h after their versions are destroyed. destroy therefore releases DataKey: every version is scheduled for destruction and its ownership labels are replaced with alchemy-released. A deploy that asks for the same key id (set cryptoKeyId to pin one) reclaims it and mints a fresh primary version. Either way, ciphertext from before the destroy can no longer be decrypted. The key ring is only removed from state; empty rings are free. The Cloud Run service, its image repository, its service account, and the secret are deleted.