1---
2title: How bindings grant IAM
3description: What a yield* of a GCP binding actually does — the per-host service account, the role it grants, and how removing a binding revokes it.
4---
5
6On GCP, giving a container permission to call an API normally means
7three separate things: create a service account, attach it to the Cloud
8Run service, and add role bindings to the project IAM policy. Alchemy
9collapses all three into the line that gets you the client.
10
11```typescript
12const events = yield* GCP.PubSub.WriteTopic(topic);
13```
14
15That expression:
16
171. Creates (or adopts) a runtime service account for this host.
182. Adds `roles/pubsub.publisher` to it on that one topic's IAM policy.
193. Injects whatever the call needs into the revision.
204. Returns a typed client (`events.publish(...)`) you call from a
21 request handler.
22
23It is the GCP counterpart of AWS `policyStatements` on a Lambda and of
24the scoped `AccountApiToken` a Cloudflare binding mints.
25
26## The per-host service account
27
28Every effectful GCP host — `GCP.Run.Service` (and its alias
29`GCP.Function`), `GCP.Run.Job`, `GCP.Run.WorkerPool`, and
30`GCP.CloudFunctions.Function` — gets its own account. The id is the
31host's name plus a hash of its type and full resource name, so no two
32hosts share an account — not across stages, and not the old and new
33generation of a replacement:
34
35```
36alch-api-3f9c0a1b2d@my-project.iam.gserviceaccount.com
37```
38
39Accounts Alchemy mints carry the display name `alchemy-host`, which is
40how `pnpm nuke:gcp` finds them. Alchemy also grants
41`roles/iam.serviceAccountUser` to the Cloud Run and Cloud Functions
42service agents so they can `actAs` the account, and retries the deploy
43while that grant propagates.
44
45Pass your own `serviceAccount` on the host and Alchemy uses it instead.
46The difference is what happens on removal, below.
47
48## Access-level clients
49
50For the data services you use most, a binding is an access level on a
51resource rather than an API operation — the same Read / Write /
52ReadWrite split as Cloudflare's R2 and KV bindings. Ask for the least
53you need; `ReadWrite` is the union of the other two.
54
55| Resource | Read | Write | ReadWrite |
56| --- | --- | --- | --- |
57| `Storage.Bucket` | `ReadBucket`: `head`, `get`, `list` | `WriteBucket`: `put`, `delete` | `ReadWriteBucket` |
58| `PubSub.Topic` | — | `WriteTopic`: `publish`, `publishBatch` | — |
59| `PubSub.Subscription` | `ReadSubscription`: `pull`, `acknowledge`, `modifyAckDeadline` | — | — |
60| `Firestore.Database` | `ReadDatabase`: `get`, `list`, `query` | `WriteDatabase`: `set`, `update`, `create`, `delete` | `ReadWriteDatabase` |
61| `BigQuery.Table` | `ReadTable`: `list`, `query` | `WriteTable`: `insert` | `ReadWriteTable` |
62| `SecretManager.Secret` | `ReadSecret`: `access`, `accessBytes` | `WriteSecret`: `addVersion`, `disableVersion`, `destroyVersion` | `ReadWriteSecret` |
63
64A topic is only ever written to and a subscription only ever read, so
65Pub/Sub has one client each. The clients speak plain JavaScript:
66Firestore fields and BigQuery rows are encoded and decoded for you,
67Pub/Sub and Secret Manager payloads come back as strings or bytes, and
68a missing document or secret version reads as `undefined` rather than
69an error.
70
71```typescript
72const db = yield* GCP.Firestore.ReadWriteDatabase(database);
73const doc = yield* db.get("counters/visits");
74const count = typeof doc?.fields.count === "number" ? doc.fields.count : 0;
75yield* db.set("counters/visits", { count: count + 1 });
76```
77
78Everything without an access-level client — KMS `Encrypt` / `Decrypt`,
79`Run.RunJob`, Cloud Tasks, Memorystore, and the long tail of generated
80services — is a per-operation binding that returns one callable.
81
82## Which role a binding grants
83
84Each binding grants the narrowest predefined role that covers it:
85
86| Binding | Role | Granted on |
87| --- | --- | --- |
88| `Storage.ReadBucket` | `roles/storage.objectViewer` | the bucket |
89| `Storage.WriteBucket`, `ReadWriteBucket` | `roles/storage.objectUser` | the bucket |
90| `PubSub.WriteTopic` | `roles/pubsub.publisher` | the topic |
91| `PubSub.ReadSubscription` | `roles/pubsub.subscriber` | the subscription |
92| `Firestore.ReadDatabase` | `roles/datastore.viewer` | the project, conditioned on the database |
93| `Firestore.WriteDatabase`, `ReadWriteDatabase` | `roles/datastore.user` | the project, conditioned on the database |
94| `BigQuery.ReadTable` | `roles/bigquery.dataViewer` + `roles/bigquery.jobUser` | the table + the project |
95| `BigQuery.WriteTable` | `roles/bigquery.dataEditor` | the table |
96| `BigQuery.ReadWriteTable` | `roles/bigquery.dataEditor` + `roles/bigquery.jobUser` | the table + the project |
97| `BigQuery.Query` | `roles/bigquery.dataViewer` + `roles/bigquery.jobUser` | the dataset's access list + the project |
98| `SecretManager.ReadSecret` | `roles/secretmanager.secretAccessor` | the secret |
99| `SecretManager.WriteSecret` | `roles/secretmanager.secretVersionManager` | the secret |
100| `SecretManager.ReadWriteSecret` | both of the above | the secret |
101| `KMS.Encrypt` / `Decrypt` | `roles/cloudkms.cryptoKeyEncrypter` / `Decrypter` | the key |
102| `CloudTasks.CreateTask` | `roles/cloudtasks.enqueuer` | the queue |
103| `Run.RunJob` | `roles/run.jobsExecutorWithOverrides` | the job |
104| `SQL.*` | `roles/cloudsql.*` | the project, conditioned on the instance |
105
106## How grants are scoped
107
108A grant lands in the narrowest place GCP lets it, in this order:
109
1101. **The resource's own IAM policy** — the GCP counterpart of an AWS
111 statement's `Resource: [arn]`. Buckets, topics, subscriptions,
112 secrets, KMS keys, Cloud Tasks queues, Run jobs and services, and
113 BigQuery tables all have one.
1142. **The dataset access list** for BigQuery datasets, which have no
115 `setIamPolicy`: the access list *is* the dataset's policy, so
116 Alchemy adds the account as an access entry.
1173. **The project policy under an IAM Condition** for services that
118 have no per-resource policy but support conditions — Firestore,
119 Cloud SQL, and Managed Kafka. The binding carries a condition like
120 `resource.name == "projects/p/databases/links" ||
121 resource.name.startsWith("projects/p/databases/links/")`, so the
122 role applies to that one resource and its children and nothing else
123 in the project.
1244. **The project policy, unconditioned**, only where GCP offers
125 neither resource IAM nor condition support: Memorystore, AlloyDB,
126 Filestore, and Vertex AI publisher models (Gemini). BigQuery's
127 `roles/bigquery.jobUser` is also project-only, because running a
128 query job is a project permission.
129
130The Memorystore RESP bindings (`ReadRedis`, `WriteRedis`,
131`ReadWriteRedis`) grant nothing at all: RESP authenticates with the
132instance's AUTH string, which travels to the runtime with the
133connection URL.
134
135IAM is eventually consistent, and conditional project grants are the
136slowest to land: expect a freshly deployed host to see `Forbidden`
137from Firestore for a few minutes.
138
139## Adding and removing
140
141The set of bindings you yield *is* the desired IAM state. The host
142records every grant it made; on the next deploy, a grant no binding
143asks for any more is revoked from that resource — delete a `yield*` and
144the permission goes away. For an Alchemy-minted account, the project
145policy is also synced against what GCP reports, so project roles added
146out of band are removed too. Accounts you supply yourself only ever
147lose grants Alchemy recorded, because Alchemy cannot tell which of the
148account's other roles matter.
149
150Policies are updated read-modify-write under their etag, so parallel
151hosts deploying against one project or topic converge instead of
152overwriting each other. Conditional bindings are matched on their exact
153condition: Alchemy only ever edits the conditional bindings it wrote,
154and never one someone else added.
155
156Destroying the host revokes every role Alchemy granted and deletes the
157minted account.
158
159## Provide the implementation
160
161Yielding a binding gives you the interface. The layer that implements
162it is provided once, at the end of the host's Effect:
163
164```typescript
165export default class Api extends GCP.Function<Api>()(
166 "Api",
167 { main: import.meta.url, location: "us-central1" },
168 Effect.gen(function* () {
169 const events = yield* GCP.PubSub.WriteTopic(topic);
170 // ...
171 }).pipe(
172 Effect.provide([GCP.PubSub.WriteTopicHttp]),
173 ),
174) {}
175```
176
177The convention is the binding name plus `Http`: `WriteTopic` →
178`WriteTopicHttp`, `ReadWriteDatabase` → `ReadWriteDatabaseHttp`,
179`RunJob` → `RunJobHttp`. Forget one and the type error names the
180missing layer.
181
182## Errors are typed
183
184Bindings return the operation's real error union, so the failure modes
185are in the type rather than in a status-code comparison:
186
187```typescript
188yield* db.create(`links/${code}`, { url }).pipe(
189 Effect.catchTag("GCP.Firestore.DocumentAlreadyExists", () =>
190 Effect.succeed(undefined),
191 ),
192);
193```
194
195`Forbidden`, `BadRequest`, and client-specific tags such as
196`DocumentAlreadyExists` or BigQuery's `InsertRowsFailed` are the ones
197you will handle most; per-operation bindings also surface `NotFound`
198and `Conflict`. Anything you do not catch stays in the channel until
199you `Effect.orDie` it.
200
201## Which host to use
202
203- **`GCP.Function`** — alias of `GCP.Run.Service`. An HTTP server with
204 a `fetch` entry. Scales to zero. Public only when you pass
205 `invokerIamDisabled: true`.
206- **`GCP.Run.Job`** — a `run` entry that executes and exits. Nothing is
207 billed between executions. Start it with a `GCP.Run.RunJob` binding
208 or Cloud Scheduler.
209- **`GCP.Run.WorkerPool`** — a long-running pool with no request path,
210 for pulling from a queue continuously.
211- **`GCP.CloudFunctions.Function`** — a gen2 function. With `main`,
212 Alchemy bundles the program for Node.js, uploads the archive, and
213 serves `fetch` through the Functions Framework, so the Effect runs
214 exactly as on a Service. Without `main` it deploys your own archive.
215
216The Run hosts build a container image locally, which means Docker has
217to be running to deploy them. Cloud Functions build in the cloud.
218
219## Event sources
220
221Bindings are calls your code makes. Event sources are the reverse:
222Google calls your code. Each one is a contract in the source service
223plus an implementation layer for the kind of host, the same split AWS
224uses for `SQS.consumeQueueMessages` with Lambda or a server host:
225
226| Consume with | Contract | Push hosts (Service, Function) | Pull hosts (Job, WorkerPool) |
227| --- | --- | --- | --- |
228| `GCP.PubSub.consumeTopicMessages` | `PubSub.TopicEventSource` | `GCP.Run.TopicEventSource` | `GCP.Run.TopicPullEventSource` |
229| `GCP.Storage.consumeBucketEvents` | `Storage.BucketEventSource` | `Storage.BucketEventSourceLive` + a topic layer | same |
230| `GCP.CloudScheduler.consumeSchedule` | `CloudScheduler.ScheduleEventSource` | `GCP.Run.ScheduleEventSource` | — |
231| `GCP.Eventarc.consumeEvents` | `Eventarc.EventSource` | `GCP.Run.EventarcEventSource` | — |
232
233```typescript
234yield* GCP.PubSub.consumeTopicMessages(orders, (messages) =>
235 messages.pipe(Stream.runForEach(({ message }) => handle(message))),
236);
237// …provided with Effect.provide(GCP.Run.TopicEventSource)
238```
239
240`GCP.Eventarc.consumeEvents` covers everything else Google emits as a
241CloudEvent: Cloud Storage and Firestore changes, Audit Log entries for
242any Google API call, and partner channels. You pass Eventarc's
243attribute filters and get each decoded CloudEvent.
244
245A push source provisions the delivery (a push subscription, a Storage
246notification, a scheduler job, an Eventarc trigger) at the host's URL on a path under
247`/__alchemy/`, signed with an OIDC token for the host's runtime service
248account, and grants that account `roles/run.invoker` on the host. At
249runtime the host claims those paths before your `fetch` and verifies
250every token's signature, audience, and email, so the service can stay
251private. A 2xx acks; a failed handler answers 500 and Pub/Sub
252redelivers. Every deploy blocks until GCP reports the delivery in place:
253IAM grants are read back, the subscription and scheduler job exist and
254are enabled, and an Eventarc trigger reports healthy. A pull source creates a subscription the host owns and runs
255a pull loop that acks each batch after the handler succeeds.
256
257## Next
258
259- [Serve an API on Cloud Run](/gcp/guides/cloud-run-api) — bindings in
260 a request path.
261- [Ingest events into BigQuery](/gcp/guides/event-pipeline) — a Service
262 triggering a Job.