Cloudflare R2

Files belong in object storage, not in the application database.

R2 is where PrimeLabs puts unstructured data: uploads, originals, exports and generated files. The application keeps a row that says who owns the object and which record it belongs to. The bytes are not a column.

A Worker reads and writes the bucket through a binding. Existing S3 tools can use the S3-compatible API. The decision to use object storage at all is on Cloudflare application development. The check that happens before a download is a Worker.

What an object store is doing in the product

R2 stores objects in buckets. An object is the file and its metadata. The key is the name, and the application chooses it: a tenant, a purpose, then the file. Buckets are the unit of access and scale. Location hints can say where the data is mostly read from. They are optional, and they are not a permission.

That is the right home for a user upload, a contract, an invoice PDF, a compliance file, a generated report, an image, a SaaS attachment, or the source document an AI knowledge system later indexes. A document workflow extracts fields from the file and stores those fields as rows. A customer portal shows the account its own files, and no one else’s.

A file, its key, and the record that points at it

Document Vault is an example bucket. Select an object to read its metadata, the application row, or an example download link. Archiving stays on this page.

Example interface. Not a client project.

Document vault · bucket client-documents · example

The signed-in user can open Northstar Services objects. Harbour & Co is a different tenant. The Worker checks that before it asks R2 for bytes.

Objects

contract-4821.pdf

Object key
northstar/onboarding/contract-4821.pdf
Size
186 KB
Content type
application/pdf
Uploaded
18 Sep 2026, 09:41
Owner
Northstar Services
Access
Private. The Worker decides.
ETag
example-9f3c1a
Retention
Onboarding, 7 years
Application record
documents.doc_4821 · request ON-2084

Example only. No file was stored or deleted.

Three ways in, and only one of them is the product

From a Worker, a binding reads, writes and lists objects without putting a storage credential in the browser. The S3-compatible API is how existing S3 SDKs and tools talk to the bucket. The Cloudflare REST API manages buckets and is the wrong path for high-throughput object traffic.

A browser that must upload or download directly needs an explicit grant. A presigned URL allows one operation on one object — get, head, put or delete — until it expires. It is a bearer token: anyone who has it can perform that operation. It works on the S3 endpoint, not on a custom domain. Short expiry and a check of who was allowed to receive the URL are the design. CORS is configured when a browser on another origin must call the bucket. A public bucket exposes objects to the internet, which is a product decision, not the default for a contract.

Knowing the key is not authorisation. The Worker checks the session and the tenant, then either fetches the object or refuses. Bucket-scoped tokens limit what an external tool can do. Adding or removing an API token’s permission can take up to a minute to be visible everywhere. Object data does not wait that long.

A write is visible to the next read

R2 is strongly consistent for the operations that matter to an application. After an upload completes, a reader sees that object. Metadata updates and deletes are visible immediately, including a delete that makes the key missing. A list shows the objects present at that moment. If two writers finish a put or a delete on the same key, the last one to complete wins. A multipart upload follows the same read-after-write rule once every part has been completed.

That guarantee is about the bucket. If a custom domain serves the object through Cloudflare’s cache, a deleted or replaced object can keep being served until the cache is purged or expires. Bindings and the S3 API talk to the bucket directly and are not affected by that cache.

Retention is a rule, not a hope

An onboarding contract and a monthly export do not have the same life. The application record stores the category. Lifecycle rules, or an explicit archive step, remove or move objects when that category says so. Deleting the row and leaving the object, or the reverse, is how a vault fills with files nobody can explain.

A bucket cannot be deleted while it still holds objects. That is a useful brake. It is not a retention policy. The policy is written down: how long, who can archive, and what the related record should say after the object is gone.

Egress is not a storage bill

Downloading an object directly from R2, including through the Workers binding, the S3 API and the managed public r2.dev hostname, does not add an egress bandwidth charge. Storage and operations are still billed. Connecting another metered service to the bucket can create a charge from that service. “No egress fees” is not “free storage”, and it is not a reason to park relational data in objects so a query can avoid a database.

When the file should be an object

Use it

  • People upload documents, images or compliance files.
  • The application generates exports, invoices or reports as files.
  • A row needs to point at an original without storing the bytes.
  • Downloads are part of the product, and egress pricing would dominate.
  • An external tool already speaks S3 and can be pointed at the bucket.

Leave it

  • The data is queried, joined or totalled. That is a database.
  • The value is a small flag or a short-lived token.
  • The object would be public when the record is confidential.
  • Nobody can say which tenant owns the key.
  • The only reason to add a bucket is that the diagram had a spare box.

Start from the file and the row that must agree

A useful note names the file, who may fetch it, how long it is kept, and which record points at the key. There is no bucket behind this page.

Discuss an application

Straight answers

Does no egress fee mean the bucket is free?

No. Storage and operations are billed. Egress bandwidth from R2 itself is not. Another service in front of the bucket can still charge for what it does.

Can the browser talk to the bucket directly?

Only with an explicit grant, such as a short-lived presigned URL for one object, or a public bucket when the files are meant to be public. The default for a private document is a Worker that has already checked the caller.

Is a deleted file gone for the next request?

Against the bucket, yes. A custom domain with caching can keep serving the old object until that cache entry is purged or expires.