Skip to content

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:

  1. The client browser asks your API for a temporary upload link.
  2. Your server validates user permissions and generates a secure upload link via storageSignedUrl with action: "write".
  3. The browser uploads the raw file with a standard HTTP PUT request 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.

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.

Terminal window
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 URL
app.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 URL
app.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)
})

On your client-side frontend application, upload the raw file using a standard JavaScript fetch request with the PUT method:

Terminal window
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.")
}
}
  1. 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.
  2. Short-Lived Access: The generated uploadUrl contains a cryptographic signature and an expiration timestamp.
  3. Upload Constraints: Layeron enforces the signed action, expiration, optional Content-Type, optional maxSizeBytes, and optional oneTime replay protection before writing the object.

Set host and path on the bucket when signed links should use a dedicated file hostname:

Terminal window
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/....