| 1 | # gcp-cloud-function |
| 2 | |
| 3 | A public JSON notes API on an Effect-native 2nd-gen Cloud Function, backed by |
| 4 | Firestore. |
| 5 | |
| 6 | | Route | Does | |
| 7 | | ------------------- | --------------------------------- | |
| 8 | | `POST /notes` | create a note from `{ title, body }` | |
| 9 | | `GET /notes` | list notes | |
| 10 | | `GET /notes/:id` | read one note | |
| 11 | | `DELETE /notes/:id` | delete a note | |
| 12 | |
| 13 | ## Architecture |
| 14 | |
| 15 | - `src/Notes.ts` — `GCP.CloudFunctions.Function` with `main: import.meta.url`. |
| 16 | Alchemy bundles it for Node.js 22, uploads the archive, and serves `fetch` |
| 17 | through the Functions Framework. No Dockerfile, no local image build. |
| 18 | - `src/resources.ts` — a named `FIRESTORE_NATIVE` `GCP.Firestore.Database` in |
| 19 | the stack's region. Each note is a document at `notes/{id}`. |
| 20 | - `alchemy.run.ts` — the stack, plus a `GCP.IAM.Member` that makes the |
| 21 | function public. |
| 22 | |
| 23 | ## Bindings and IAM |
| 24 | |
| 25 | | Binding | Grants | |
| 26 | | ---------------------------------------------------------------- | ---------------------------------------------------------------- | |
| 27 | | `GCP.Firestore.ReadWriteDatabase` (`ReadWriteDatabaseHttp`) | `roles/datastore.user` to the function's runtime service account, on the project under an IAM Condition matching only `NotesDb` | |
| 28 | | `GCP.IAM.Member("PublicInvoker", …)` on the function's Cloud Run service | `roles/run.invoker` to `allUsers` | |
| 29 | |
| 30 | Alchemy mints a dedicated runtime service account for the function; the |
| 31 | Firestore grant goes to that account only. Firestore databases have no |
| 32 | resource-level IAM policy, so the grant sits on the project with the |
| 33 | condition `resource.name == "<db>" || resource.name.startsWith("<db>/")`. |
| 34 | |
| 35 | Drop the `PublicInvoker` member to keep the function private — callers then |
| 36 | need a Google identity token. |
| 37 | |
| 38 | ## Deploy |
| 39 | |
| 40 | 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>`. |
| 41 | |
| 42 | ```sh |
| 43 | pnpm deploy --profile <name> |
| 44 | ``` |
| 45 | |
| 46 | The first deploy takes a few minutes: Cloud Build builds the function and |
| 47 | Firestore provisions the database. A fresh Firestore grant can take a few |
| 48 | more minutes to propagate, during which the API answers `500`. |
| 49 | |
| 50 | ```sh |
| 51 | curl -X POST "$url/notes" -H 'content-type: application/json' \ |
| 52 | -d '{"title":"groceries","body":"eggs, milk"}' |
| 53 | curl "$url/notes" |
| 54 | ``` |
| 55 | |
| 56 | ## Test |
| 57 | |
| 58 | ```sh |
| 59 | ALCHEMY_PROFILE=<name> bun test |
| 60 | ``` |
| 61 | |
| 62 | Deploys the stack, drives the full CRUD cycle over HTTP, checks the |
| 63 | document in Firestore directly, then destroys the stack and verifies the |
| 64 | function and database are gone. |
| 65 | |
| 66 | ## Destroy |
| 67 | |
| 68 | ```sh |
| 69 | pnpm destroy --profile <name> |
| 70 | ``` |