R2 foundations
Model buckets, objects, and keys
Treat R2 as object storage with application-owned naming and authorization rather than a mounted relational filesystem.
8 minute lesson
R2 stores objects in buckets. An object is a body of bytes plus a key, HTTP metadata such as the content type, optional custom metadata, and version information such as an entity tag. That is the entire model. No folders, no rows, no indexes.
If you’ve used Amazon S3, it’s the same idea. The pricing is the big difference: R2 charges nothing for egress, so serving files is cheap.
Create a bucket and bind it to a Worker:
npx wrangler r2 bucket create my-app-files
{
"r2_buckets": [
{ "binding": "FILES", "bucket_name": "my-app-files" }
]
}
Now env.FILES is the bucket in your code.
Keys are names, not paths
A key like tenant/123/uploads/report.pdf looks like a filesystem path, but the namespace is flat. The slashes are just characters in the name. Keys can look like paths, but R2 does not become a POSIX filesystem or relational database.
What a shared prefix gives you is cheap grouping. You can list everything under it:
const list = await env.FILES.list({ prefix: 'tenant/123/uploads/' })
for (const object of list.objects) {
console.log(object.key, object.size, object.etag)
}
Each entry carries the key, the size, the upload time, and the entity tag. There is no “rename folder” operation, because there are no folders. Changing a prefix means copying objects to new keys.
Design the namespace first
My advice is to put the owner first, then the purpose, then a server-generated name: tenant/123/uploads/<uuid>. The server picks the UUID, so two uploads can never collide and a user can never overwrite someone else’s file.
R2 only finds objects by key or prefix. Questions like “which files did this user upload last week” need a real query engine, so keep searchable relational metadata in D1: one row per object, keyed by the R2 key.
The classic early mistake is building the key from a request parameter. If the client sends ?path=tenant/456/uploads/x.pdf and you use it as the key, one tenant reads another tenant’s files. Take the tenant ID from the authenticated session, never from the request. Prove one tenant cannot choose another tenant’s prefix before you ship anything else.
Lesson completed