Skip to content
Source
Sign inSign up

.repos/alchemy-effect/examples/gcp-cloud-run-api

Contents of .repos/alchemy-effect/examples/gcp-cloud-run-api
NameLast commit messageLast commit date
..
src (directory)
test (directory)
alchemy.run.ts
package.json
README.md
tsconfig.json

README.md

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

HostBindingIAM granted to the host's service account
ApiGCP.Firestore.ReadWriteDatabase(Links) + ReadWriteDatabaseHttproles/datastore.user on the project, with an IAM Condition limiting it to Links
ApiGCP.SecretManager.ReadSecret(ApiKey) + ReadSecretHttproles/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.