Pre-signed uploads
Uploading large files (such as user videos, PDF reports, or high-resolution images) directly through your gateway worker is inefficient. It consumes expensive CPU time, exhausts memory buffers, and is capped by Cloudflare’s HTTP payload limit (100 MB).
The industry-standard solution is Pre-signed Upload URLs:
- The client browser asks your API for a temporary upload link.
- Your server validates user permissions and generates a secure upload link via
storageSignedUrlwithaction: "write". - The browser uploads the raw file with a standard HTTP
PUTrequest to the signed URL.
Layeron creates and manages the Cloudflare R2 API token used for default bucket signed URLs. You do not create a user binding for that token. If the bucket uses managed encryption, Layeron signs the URL with the encryption Secret and routes the upload through the Storage Product Worker so the stored object is encrypted.
Code Implementation
Section titled “Code Implementation”This example shows how to declare a private bucket, write an endpoint to generate a pre-signed upload URL for user avatars, and retrieve it.
import { backend } from "@layeron/core"import { storage, storageSignedUrl } from "@layeron/modules"
const app = backend()
// 1. Declare a private R2 bucket.const avatarsBucket = storage.bucket({ name: "user-avatars", access: "private",})
app.use(avatarsBucket)
// 2. Route: Generate Pre-signed Upload URLapp.post("/api/avatar/upload-link", async (request) => { // E.g. authenticate user first... const userId = "usr_100" const key = `avatars/${userId}.png`
// Generate a temporary write URL valid for 5 minutes (300 seconds) const uploadUrl = await storageSignedUrl(avatarsBucket, key, { action: "write", expiresInSeconds: 300, contentType: "image/png", maxSizeBytes: 5 * 1024 * 1024, // Enforce a max size of 5 MB oneTime: true, // Reject retries or replay after the first valid request })
return Response.json({ uploadUrl, key, })})
// 3. Route: Serve Avatar Image via Download pre-signed URLapp.get("/api/avatar", async (request) => { const userId = "usr_100" const key = `avatars/${userId}.png`
// Verify the image exists first const exists = await avatarsBucket.head(key) if (!exists) { return new Response("No avatar found", { status: 404 }) }
// Generate a temporary read URL valid for 10 minutes const downloadUrl = await storageSignedUrl(avatarsBucket, key, { action: "read", expiresInSeconds: 600, })
// Redirect the browser to download directly from the secure CDN return Response.redirect(downloadUrl, 302)})Client-Side Browser Code (Frontend)
Section titled “Client-Side Browser Code (Frontend)”On your client-side frontend application, upload the raw file using a standard JavaScript fetch request with the PUT method:
async function uploadAvatar(file) { // A. Ask your Layeron backend for a pre-signed write link const response = await fetch("/api/avatar/upload-link", { method: "POST" }) const { uploadUrl } = await response.json()
// B. Perform HTTP PUT directly to the R2 uploadUrl const uploadResult = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": "image/png", // Must match the contentType configured in storageSignedUrl. }, body: file, // Raw binary file (Blob / File object) })
if (uploadResult.ok) { console.log("Avatar uploaded successfully!") } else { console.error("Upload failed.") }}How It Works
Section titled “How It Works”- Authentication Guard: Your backend controls the folder structure and verifies the user is authorized before granting the pre-signed link. The client never gets direct access to arbitrary directories.
- Short-Lived Access: The generated
uploadUrlcontains a cryptographic signature and an expiration timestamp. - Upload Constraints: Layeron enforces the signed action, expiration, optional
Content-Type, optionalmaxSizeBytes, and optionaloneTimereplay protection before writing the object.
Custom Signed URL Host
Section titled “Custom Signed URL Host”Set host and path on the bucket when signed links should use a dedicated file hostname:
const avatarsBucket = storage.bucket({ name: "user-avatars", access: "private", host: "files.example.com", path: "/__layeron/r2",})The URL returned by storageSignedUrl() uses the configured prefix, such as https://files.example.com/__layeron/r2/....