Modern applications often need to store profile pictures, PDFs, documents, videos, and other media. Instead of storing these files directly in a database, we can use object storage.
Let's understand the complete flow with a simple example: uploading a profile picture.
R2 and the Database
Suppose a user wants to upload:
profile-image.png
We separate the actual file from its application metadata:
DATABASE CLOUDFLARE R2
──────── ─────────────
userId Actual image
fileKey ──────────────────────> users/123/profile/abc.png
fileName
mimeType
fileSize
The actual image is stored in R2, while our database stores information needed to reference and manage it.
How Does the Upload Work?
The basic architecture is:
Frontend
│
│ 1. Request upload
▼
Backend
│
│ 2. Validate file information
│ 3. Generate unique fileKey
│ 4. Generate Presigned PUT URL
▼
Frontend
│
│ 5. PUT actual file
▼
Cloudflare R2
│
▼
Object Stored
1. Frontend requests an upload
The frontend first sends information about the selected file:
POST /media/upload-url
{
"fileName": "profile-image.png",
"mimeType": "image/png",
"fileSize": 245760
}
At this point, the actual image has not been uploaded.
2. Backend validates and generates a file key
The backend validates information such as the allowed file type and size.
It then generates a unique object key:
users/123/profile/550e8400.png
This fileKey identifies where the object will be stored inside the R2 bucket.
3. Backend generates a Presigned PUT URL
Using server-side R2 credentials, the backend generates a temporary presigned PUT URL for that specific object key.
For example:
{
"fileKey": "users/123/profile/550e8400.png",
"uploadUrl": "<presigned-url>",
"expiresIn": 600
}
Conceptually, the URL provides temporary permission for:
Operation → PUT
Object → users/123/profile/550e8400.png
Expiry → 10 minutes
The R2 credentials remain securely on the backend.
4. Frontend uploads directly to R2
The frontend now sends the actual image using the presigned URL:
PUT <presigned-url>
Content-Type: image/png
<actual image bytes>
The important architecture is:
Frontend ───── actual file ─────> R2
The large file doesn't need to travel through the application backend in this direct-upload design.
R2 validates the signed request and stores the object under the key used to create the URL.
5. Store the reference
After a successful upload, the application can save or finalize the metadata:
userId: 123
fileKey: users/123/profile/550e8400.png
fileName: profile-image.png
mimeType: image/png
fileSize: 245760
A more robust application can also call something like:
POST /media/complete
after the R2 upload succeeds, allowing the backend to verify/finalize the uploaded media.
What Happens When the Presigned URL Expires?
Suppose the URL is valid for 10 minutes.
After 10 minutes:
Presigned URL ❌ Expired
Uploaded R2 object ✅ Still exists
The permission is temporary, not the stored object.
Final Flow
The entire upload process can be remembered as:
Select File
↓
POST metadata → Backend
↓
Validate
↓
Generate File Key
↓
Generate Presigned PUT URL
↓
Return URL + File Key
↓
Frontend PUT → R2
↓
Actual File Stored
↓
Store / Finalize Metadata
The core idea is simple:
The backend controls the upload, the presigned URL provides temporary permission, the frontend uploads directly to R2, R2 stores the actual file, and the database stores its reference.

Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.