t3-code-android-nightly/.repos/alchemy-effect/examples/gcp-cloud-run-api
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

Link shortener on Cloud Run

A public link shortener: one Cloud Run container, Firestore for state, and Secret Manager for the API key that guards writes. Every IAM grant comes from a binding, so nothing is wired by hand.

curl -X POST "$URL/links" -H "x-api-key: my-key" -H 'content-type: application/json' -d '{"url":"https://alchemy.run"}'
# 201 {"code":"aZ3k9Qx","shortUrl":"https://…/l/aZ3k9Qx"}
curl -i "$URL/l/aZ3k9Qx"
# 302 location: https://alchemy.run
curl "$URL/links/aZ3k9Qx"
# 200 {"code":"aZ3k9Qx","url":"https://alchemy.run","clicks":1,…}

Architecture

  • Links — named Firestore Native database; one document per link at links/{code}.
  • ApiKey — Secret Manager secret holding the key callers send as x-api-key. Alchemy creates the secret; adding a version with the value is an operator step.
  • Api — public GCP.Function (a Cloud Run service):
    • GET / — health check.
    • POST /links — mint a 7-character base62 code for a URL (requires x-api-key).
    • GET /l/:code — 302 to the target and count the click.
    • GET /links/:code — read the link back.
    • DELETE /links/:code — retire a code (requires x-api-key).

Until the secret has a version, POST /links answers 503 instead of accepting any key.

Bindings and IAM

Host Binding IAM granted to the host's service account
Api GCP.Firestore.ReadWriteDatabase(Links) + ReadWriteDatabaseHttp roles/datastore.user on the project, with an IAM Condition limiting it to Links
Api GCP.SecretManager.ReadSecret(ApiKey) + ReadSecretHttp roles/secretmanager.secretAccessor on the ApiKey secret only

Firestore databases have no resource-level IAM policy, so the Firestore grant is project-level under an IAM Condition on the database name. It can take a few minutes to propagate after the first deploy; until then writes fail with 500.

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>.

From the repository root:

pnpm install
cd examples/gcp-cloud-run-api
pnpm deploy --profile <name>

Docker must be running, because the service is built locally from main. Then add the API key:

printf 'my-key' | gcloud secrets versions add "<secretId output>" --data-file=-

Live test

ALCHEMY_PROFILE=<name> bun test --timeout 1200000

The test deploys the stack, adds a secret version, and checks the health route. It verifies that creates without the right key get 401, then mints a link, confirms the Firestore document out of band, follows the redirect, checks the click count, and deletes the link. At the end it destroys the stack.

Destroy

pnpm destroy --profile <name>

This deletes the service, its IAM grants, the secret with all of its versions, and the database with all of its documents.