Skip to content
Engineering

What Is R2 Storage? Private File Uploads for BYOB Apps

BYOB Team

BYOB Team

Updated:
10 min read

R2 Storage gives BYOB apps private by default object storage for uploads, images, and media. Uploads and downloads pass through SvelteKit server routes that check the session and validate files, with credentials staying inside the worker. This guide explains the STORAGE binding, app signed URLs for sharing, and smoke checks that prove uploads work.

Key takeaways

  • • R2 is S3 compatible object storage for files and media
  • • BYOB uses private by default proxy routes through SvelteKit
  • • The STORAGE binding keeps credentials inside the worker environment
  • • App signed URLs allow secure sharing without exposing bucket endpoints
What Is R2 Storage? Private File Uploads for BYOB Apps

Short answer, private files without credential sprawl #

R2 Storage gives BYOB apps S3-compatible object storage for uploads, images, and media. Every modern web application needs file storage, whether for user avatars, document uploads, or media assets. BYOB provides a one-click integration that wires an R2 bucket directly into your SvelteKit project.

The important part is the security pattern. Files are private by default. Uploads and downloads go through your app server routes, not directly from the browser to the bucket. When you need to share a private file, BYOB uses app-signed route URLs that your app can verify before streaming anything.

This post is a learn/define explainer of what R2 is and how the private-by-default proxy pattern works. If you want the do/procedural setup instead — attach the STORAGE binding and route uploads through server routes — see Mastering BYOB Storage.

Picture a storage facility with a strict front desk. Renters never get the master key to the building. They request a box through the desk, the clerk checks their ID, walks back, and returns with exactly that box. R2 is the building. Your SvelteKit routes are the desk. Credentials never leave the back room.

✨
TIP

Try it: Image Compressor — so you can try this on your own site now.

Try it right here: image compressor resizerOpen full tool

Loading the interactive tool… or open it here.

flowchart LR A[Upload file] --> B[Store object in R2] B --> C[Serve by URL] C --> D[App loads media]

What is R2? #

R2 is S3-compatible object storage, as documented by Cloudflare (https://developers.cloudflare.com/r2/). Object storage means you store files as objects, images, PDFs, audio, video, and other uploads, rather than rows in a database. S3-compatible means it follows the bucket and object concepts AWS defined for S3 (https://docs.aws.amazon.com/s3/), so skills transfer and tooling feels familiar.

Two properties shape every decision. Objects are blobs with keys, not rows with schemas, so reach for R2 when the payload is a file and for a database when the payload is structured data you query. And buckets are flat namespaces with key prefixes standing in for folders, so design key layouts like avatars/{userId}/{file} from day one instead of improvising later.

In BYOB, you do not manage raw access keys or complex environment variables for this path. When storage is enabled for a project, BYOB attaches an R2 bucket to the worker runtime as the STORAGE binding. Your SvelteKit backend can then access the bucket natively through a server helper. That helper checks the worker environment for STORAGE and reports a clear error if file storage is not connected. This keeps configuration explicit. If storage was never enabled, the app says so instead of failing in a confusing way. The full integration, including bucket setup and helper wiring, is walked through in the storage guide (https://byob.studio/blog/byob-storage-r2).

Pattern How access works Credential place When to use
Private proxy SvelteKit route checks session Worker binding Default for user files
App signed URL Token checked by your route App signed route Share without public bucket
Public bucket Direct bucket URL None Marketing assets only
Supabase Storage RLS policy Postgres auth When using Supabase path

How does private by default plus signed URLs work? #

BYOB enforces a secure-by-default proxy pattern. All uploads and downloads go through SvelteKit server routes, such as an upload route under your API path. That route does three jobs before touching the bucket.

First, authentication. The server route verifies the user session through Better Auth before interacting with storage, following the auth integration (https://byob.studio/blog/byob-auth-better-auth-d1). Anonymous or signed-out requests do not get free access to other users files. Identity first, bytes later.

Second, validation. File types and sizes are checked securely on the server. Browser-side checks alone are not enough, because they can be bypassed with a curl command in seconds. Check the magic bytes or at minimum the MIME type server-side, enforce a size cap that matches your use case, and reject everything else with a clear error.

A typical upload route follows this shape:

js
// src/routes/api/uploads/+server.js
import { json, error } from '@sveltejs/kit';

const MAX_BYTES = 5 * 1024 * 1024; // 5 MB avatars, adjust per use case
const ALLOWED = new Set(['image/png', 'image/jpeg', 'image/webp']);

export async function POST({ request, locals, platform }) {
  const session = await locals.auth();
  if (!session?.user) throw error(401, 'Sign in to upload');
  const form = await request.formData();
  const file = form.get('file');
  if (!(file instanceof File)) throw error(400, 'Missing file');
  if (!ALLOWED.has(file.type)) throw error(400, 'Unsupported type');
  if (file.size > MAX_BYTES) throw error(400, 'File too large');
  const key = `avatars/${session.user.id}/${crypto.randomUUID()}`;
  await platform.env.STORAGE.put(key, await file.arrayBuffer(), {
    httpMetadata: { contentType: file.type }
  });
  return json({ key });
}

Third, secrecy. Cloudflare credentials never leave the worker environment. The browser talks to your app, your app talks to the bucket through the STORAGE binding, and raw bucket access is never hardcoded into client code. A common anti-pattern is exposing raw presigned bucket URLs directly to the browser or shipping credentials in the client bundle. BYOB avoids that by generating app-signed route URLs instead. Rather than pointing at the bucket endpoint, the signed URL points back to your SvelteKit app, for example a file route with a token parameter. The route validates the token and streams the file from the STORAGE binding. Because the URL stays on your domain, you can also add authorization logic or tracking around access without changing where files are stored.

What are typical uses for images, uploads, and media? #

Use R2 whenever the app needs durable files attached to users or content.

Design key layouts before the first upload, because keys are the only index objects have. Prefix with owner and purpose, such as avatars/{userId}/{uuid} or invoices/{year}/{id}, so listing by prefix answers the common queries without a database roundtrip. Keep keys opaque to outsiders: sequential ids leak counts, while random suffixes reveal nothing. Store the key beside the database row that owns it, and treat the pair as one record split across two systems. Deletes must remove both, or orphaned objects accumulate into a bill nobody can explain.

User avatars are the classic case. A visitor signs up through Better Auth, uploads a profile image through your upload route, and the app stores the object reference alongside the user record. Later downloads go through the same authorized route pattern. Keep originals plus one display size rather than five speculative sizes. You can add variants when traffic justifies them.

Document uploads follow the same shape. Contracts, resumes, worksheets, receipts, and other files are uploaded by authenticated users, validated for type and size on the server, then retrieved only when the session and token checks pass. For documents, log access. A download ledger turns who saw this file from a mystery into a query.

Media assets are the third common use. Hero images, gallery photos, audio clips, and other site content benefit from object storage plus CDN-backed delivery, especially once you publish and republish the production build. Large public assets can be served with long cache lifetimes since content-hashed keys make invalidation safe.

If your project explicitly uses Supabase instead, use that distinct path. BYOB supports Supabase Storage alongside Supabase Postgres with the appropriate client and bucket policy patterns, as covered in the Supabase storage docs (https://supabase.com/docs/guides/storage). Do not mix R2 bindings and Supabase Storage in the same flow. Choose one based on your database and infrastructure decision, because two storage systems means two permission models and twice the audit surface.

What smoke checks prove uploads work? #

Prove R2 uploads work in BYOB with 6 smoke checks: confirm storage is connected, round-trip an upload and download as a signed-in user, confirm signed-out access is blocked, submit an invalid type and an oversized file, open a signed URL with a missing or altered token, then republish and retest. Prerequisites: a project with storage enabled plus a signed-in session. Constraint: server-side validation decides; browser checks alone never count.

Before treating uploads as done, run these simple checks in the preview and after deploy.

  • Confirm storage is connected. A clear not-connected error means the bucket was never enabled for that project, not that your upload code is wrong.
  • Upload as a signed-in user, then download through the app route. The file should round-trip without exposing a bucket endpoint in the browser address bar or network panel.
  • Confirm signed-out access is blocked. Requests without a valid session should not be able to upload or fetch private files.
  • Try an invalid type or oversized file. Server-side validation should reject it even if the browser form allowed it.
  • Open an app-signed file URL with a missing or altered token. It should fail validation rather than stream the file.
  • Republish and retest. Your published site updates on every deploy while the URL stays the same, so verify that previously uploaded files and new uploads both behave after an update.

If a check fails, refine with a specific prompt. Say exactly what happened, which route was used, whether the user was signed in, and what you expected instead. Specific feedback produces an exact fix, while vague reports invite guesswork. Add these checks to your launch list beside domain and deploy steps. Storage failures surface at the worst moments, usually during someone else's demo, and a five-minute checklist beats a five-hour postmortem every time. Files are forever, so prove the desk checks IDs before the crowd arrives.

What are the trade-offs? #

Private by default proxy routes plus app signed URLs win when files belong to users. Uploads and downloads pass through SvelteKit server routes that check the session and validate type and size, the STORAGE binding keeps credentials inside the worker, and sharing uses app routes that validate tokens before streaming.

Pick R2 in BYOB when Pick Supabase Storage when
The stack is worker native with D1 beside it Rows already live in Supabase Postgres with RLS
Prefixes like avatars/{userId}/{uuid} plus a ledger fit the model One permission story must cover rows and files together
Smoke checks prove round trip, blocked signed out access, and rejected bad types The team already audits buckets through Supabase policies

Mixing both in one flow loses: two permission models double the audit surface, and deletes must remove row plus object or orphans bill quietly. Pick one path per flow and keep it consistent.

Who this is for (and who should skip it) #

R2 Storage in BYOB fits apps handling uploads, avatars, or media that must stay private by default behind SvelteKit routes with the STORAGE binding. Pick managed Postgres storage where data already lives in Supabase and one RLS story must cover rows and files. Limit: mixing both doubles permission models, and deletes must remove the database row plus the object.

This fits if your app handles uploads, avatars, or media and you want private by default files served through your app rather than public bucket links. Worker bound storage keeps credentials out of the browser.

Pick managed Postgres storage where your data already lives in Supabase and you want one RLS story for both rows and files. Choose one path and keep it consistent.

One limit to know. Mixing R2 and Supabase Storage doubles permission models, so pick one path per flow. Deletes must remove both the database row and the object, or orphaned files pile into an unexplained bill.

  • Best for developers storing avatars and uploads as private objects behind app routes.
  • Best for startups sharing files with signed URLs instead of leaking bucket credentials.
  • Best for freelancers wiring client media uploads that stay secure by default.

What we learned building this #

BYOB connects file storage so bucket credentials stay inside the hosting environment and file access goes through the app. Uploads and downloads pass through server routes that check the session and validate files, and sharing uses app-signed links that the app verifies before streaming. The storage guide shows the private-by-default pattern we use in the product.

How we picked these

Checked private proxy and signed URL claims against Cloudflare R2, AWS S3, Supabase Storage, and BYOB storage posts; no upload was tested live.

Frequently asked questions

What is R2 Storage in BYOB?

It is S3 compatible object storage wired into your SvelteKit app through a STORAGE binding, used for avatars, uploads, documents, and media assets.

How do private files stay secure?

All uploads and downloads go through SvelteKit server routes that verify the user session, validate file types and sizes, and keep credentials inside the worker.

How do I share a private file safely?

Use an app signed route URL that points back to your app. The route validates the token before streaming the file from storage.

Should I mix R2 and Supabase Storage in one flow?

No. Choose one based on your database decision. R2 pairs with the worker native stack while Supabase Storage pairs with Supabase Postgres

Changelog

  • • Added fit guide, comparison table, and hands on notes
  • • Added trade-offs section plus question form H2 pass (Sep 2026)
  • • Intent split 2026-09-23 - explainer lane learn/define plus early cross-link to byob-storage-r2

About the Author

BYOB Team

BYOB Team

The creative minds behind BYOB. We're a diverse team of engineers, designers, and AI specialists dedicated to making web development accessible to everyone.

Ready to start building?

Join thousands of developers using BYOB to ship faster with AI-powered development.

Get Started Free