1---
2title: R2
3description: Cloudflare R2 is object storage — create a bucket, bind it into a Worker with read/write-scoped bindings, and stream objects in and out.
4---
5
6R2 is Cloudflare's object storage: buckets of arbitrarily large
7objects addressed by key, with an S3-compatible API. In alchemy an
8R2 bucket is a one-line resource, and Workers reach it through
9typed, access-scoped bindings.
10
11Reach for R2 whenever you need to store files: uploads, build
12artifacts, media, backups, or document corpora for
13[AI Search](/cloudflare/ai/ai-search).
14
15## Create a bucket
16
17```typescript
18// src/bucket.ts
19import * as Cloudflare from "alchemy/Cloudflare";
20
21export const Bucket = Cloudflare.R2.Bucket("Bucket");
22```
23
24Yield it inside your Stack:
25
26```typescript
27// alchemy.run.ts
28import * as Alchemy from "alchemy";
29import * as Cloudflare from "alchemy/Cloudflare";
30import * as Effect from "effect/Effect";
31import { Bucket } from "./src/bucket.ts";
32
33export default Alchemy.Stack(
34 "MyApp",
35 {
36 providers: Cloudflare.providers(),
37 state: Cloudflare.state(),
38 },
39 Effect.gen(function* () {
40 const bucket = yield* Bucket;
41
42 return { bucketName: bucket.bucketName };
43 }),
44);
45```
46
47No name required — alchemy generates a unique one from the app,
48stage, and logical ID.
49
50## Bind it to a Worker
51
52Bind the bucket in the Worker's Construction phase and use the typed client
53in the runtime handlers. This Worker stores objects on `PUT /:key`
54and serves them back on `GET /:key`:
55
56```typescript
57// src/worker.ts
58import * as Cloudflare from "alchemy/Cloudflare";
59import * as Effect from "effect/Effect";
60import { HttpServerRequest } from "effect/http/HttpServerRequest";
61import * as HttpServerResponse from "effect/http/HttpServerResponse";
62import { Bucket } from "./bucket.ts";
63
64export default Cloudflare.Worker(
65 "Worker",
66 { main: import.meta.url },
67 Effect.gen(function* () {
68 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
69
70 return {
71 fetch: Effect.gen(function* () {
72 const request = yield* HttpServerRequest;
73 const key = request.url.split("/").pop()!;
74
75 if (request.method === "PUT") {
76 yield* bucket.put(key, request.stream, {
77 contentLength: Number(request.headers["content-length"] ?? 0),
78 });
79 return HttpServerResponse.empty({ status: 201 });
80 }
81
82 const object = yield* bucket.get(key);
83 if (object === null) {
84 return HttpServerResponse.text("Not found", { status: 404 });
85 }
86 return HttpServerResponse.text(yield* object.text());
87 }).pipe(
88 Effect.catchTag("R2Error", (error) =>
89 Effect.succeed(
90 HttpServerResponse.text(error.message, { status: 500 }),
91 ),
92 ),
93 ),
94 };
95 }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
96);
97```
98
99R2 operations can fail with a typed `R2Error` — Effect keeps it in
100the type system until you handle it, here with `Effect.catchTag`.
101[Tutorial part 2](/cloudflare/tutorial/part-2) builds this Worker
102step by step.
103
104## Read, Write, ReadWrite
105
106The bucket capability is split by access level so each Worker gets
107least privilege:
108
109- `Cloudflare.R2.ReadBucket` — `head`, `get`, `list`.
110- `Cloudflare.R2.WriteBucket` — `put`, `delete`, multipart uploads.
111- `Cloudflare.R2.ReadWriteBucket` — both.
112
113Each level has two interchangeable implementations you provide as a
114layer on the Worker:
115
116- **`*BucketBinding`** (`ReadBucketBinding`, `WriteBucketBinding`,
117 `ReadWriteBucketBinding`) — a native R2 binding on the Worker,
118 the default choice.
119- **`*BucketHttp`** (`ReadBucketHttp`, `WriteBucketHttp`,
120 `ReadWriteBucketHttp`) — the same client over Cloudflare's HTTP
121 API with a scoped API token, for when a native binding isn't
122 available.
123
124Your runtime code depends only on the capability
125(`ReadBucket(Bucket)` etc.) — swapping the implementation layer
126doesn't touch the handlers.
127
128## Presigned URLs
129
130To let browsers upload or download objects directly, mint presigned
131URLs with `Cloudflare.R2.PresignPutObject` / `PresignGetObject`, or
132declare `Cloudflare.R2.S3Credentials` on an async Worker. The same
133code works under `alchemy dev`, where the local bucket is served on
134an S3-compatible endpoint. See
135[R2 presigned URLs](/cloudflare/data/r2-presigned-urls).
136
137## Public development URL
138
139Buckets are private by default. Set `publicAccess: true` to serve
140objects from Cloudflare's managed `r2.dev` domain — useful for
141preview stacks and anywhere a signed S3 URL or a custom domain
142isn't an option. The hostname lands on `publicDomain`:
143
144```typescript
145const bucket = yield* Cloudflare.R2.Bucket("Assets", {
146 publicAccess: true,
147});
148// https://pub-….r2.dev/photo.jpg
149```
150
151This endpoint is rate-limited and intended for non-production use.
152For production traffic, attach a hostname you control with
153`domains`.
154
155## Destroying a bucket
156
157R2 refuses to delete a bucket that still has objects in it, and
158alchemy does not bypass that refusal: destroying a non-empty bucket
159fails with `BucketNotEmpty`, leaving the bucket and its objects
160intact. For buckets whose contents are disposable, opt into emptying
161them first:
162
163```typescript
164const cache = yield* Cloudflare.R2.Bucket("Cache", {
165 forceDestroy: true,
166});
167```
168
169To keep a bucket even when its declaration goes away, mark it
170`retain` — alchemy drops it from state and never calls delete:
171
172```typescript
173import * as RemovalPolicy from "alchemy/RemovalPolicy";
174
175const uploads = yield* Cloudflare.R2.Bucket("Uploads").pipe(
176 RemovalPolicy.retain(),
177);
178```
179
180See [Resource lifecycle › Removal policy](/infrastructure-as-code/resource-lifecycle#removal-policy).
181
182## Where next
183
184- [Tutorial part 1](/cloudflare/tutorial/part-1) — deploy your first
185 bucket.
186- [R2 presigned URLs](/cloudflare/data/r2-presigned-urls) — direct
187 browser uploads and downloads, locally and deployed.
188- [Add AI Search (AutoRAG)](/cloudflare/ai/ai-search) — RAG over
189 documents stored in R2.
190
191Related:
192
193- [Workers](/cloudflare/compute/workers) — how buckets get read and written.
194
195Reference:
196
197- [Bucket API reference](/providers/cloudflare/r2/bucket)