Manage your project storage ->
Mastering BYOB storage (Cloudflare R2 integration) #
Think of object storage as a warehouse. Anyone can rent shelf space. The shops that survive are the ones with a guarded loading dock, where every box gets checked before it goes in or out.
BYOB gives you that dock. When you enable storage, we attach a Cloudflare R2 bucket to your project as the STORAGE binding and route every upload and download through SvelteKit server routes that check who you are and what the file is. No raw credentials in client code. No public bucket endpoints. Just a clean pipe from browser to bucket with a bouncer in the middle.
[!TIP] Try it: Image Compressor
How does the STORAGE binding connect your bucket? #
Cloudflare describes a binding as a runtime variable that the Workers runtime provides to your code, declared in the Wrangler file and bound to a resource such as an R2 bucket, as stated in the Cloudflare R2 Workers API docs (https://developers.cloudflare.com/r2/api/workers/workers-api-usage/). You pick the variable name, Cloudflare wires it to the bucket, and your Worker reads and writes through it with no network hop and no access keys.
BYOB does that wiring for you at project setup. Your SvelteKit backend then reaches the bucket through a small server helper:
// src/lib/server/storage.ts
import { error, type RequestEvent } from '@sveltejs/kit';
export function getStorage(event: RequestEvent) {
const bucket = event.platform?.env?.STORAGE;
if (!bucket) throw error(503, 'File storage is not connected.');
return bucket;
}Two details matter here. First, the helper fails loudly with a 503 when storage is not connected, so a misconfigured project tells you what is wrong instead of failing in strange ways later. Second, nothing in this file is a secret. The binding is infrastructure, not a password, which means there is nothing to rotate and nothing to leak into a client bundle.
SvelteKit's filesystem router makes the other half natural. Routes map to files, and +server.js files act as API endpoints that run only on the server, as stated in the SvelteKit routing docs (https://svelte.dev/docs/kit/routing). BYOB leans on that split. Browser code talks to /api/storage/* routes. Only those routes ever touch the bucket.
What does R2 cost for a typical project? #
R2 Standard storage is priced at $0.015 per GB-month, Class A operations at $4.50 per million, Class B at $0.36 per million, and egress to the internet is free, as stated in the Cloudflare R2 pricing page (https://developers.cloudflare.com/r2/pricing/). The monthly free tier covers 10 GB-months of storage, 1 million Class A requests, and 10 million Class B requests.
For a typical young project, avatars plus a few hundred uploads, that means storage costs round to zero. The number worth watching is not gigabytes. It is request volume once you start serving images at scale. At that point the zero egress fee is the quiet win, since outbound bandwidth is where traditional object storage bills tend to bite.
One honest caveat. The free tier applies to Standard storage only. If you ever move cold archives to Infrequent Access, different rates and a 30-day minimum storage duration apply. BYOB defaults to the Standard path, so you will not trip over that by accident.
Why do uploads pass through server routes? #
A common anti-pattern is handing the browser a raw presigned URL or, worse, baking credentials into client code. It works until it does not, and when it breaks it breaks in the open.
BYOB enforces a secure-by-default proxy pattern. Uploads and downloads travel through your SvelteKit server routes, something like /api/storage/upload, and each route does three jobs.
It verifies the session before touching the bucket, so anonymous traffic never reaches storage. It validates file type and size on the server as well as in the browser, since client checks are suggestions and server checks are law. And it keeps every credential inside the worker environment, where frontend code cannot see it.
This matches what OWASP prescribes for file uploads: restrict uploads to what the business needs, validate with an allowlist rather than a denylist, treat filenames and metadata as hostile input, and scan content before serving it to other users, as stated in the OWASP file upload guide (https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload). Our generated routes follow that shape, with server-side allowlists for MIME types, byte caps per upload, and sanitized object keys instead of raw user filenames.
A practical example helps. Say users upload profile photos. The route accepts the multipart body, rejects anything that is not JPEG, PNG, or WebP, rejects anything over 5 MB, writes it under a generated key like avatars/{userId}/{uuid}.webp, and returns the key. The browser never learns the bucket name. The bucket never learns about the browser.
App-signed URLs #
Public assets are simple. Serve them from a public path and move on. Private files need more care.
BYOB generates app-signed route URLs for private sharing. Instead of a direct R2 presigned URL that exposes the bucket endpoint, you get a signed link pointing back at your own app, like /api/storage/file?token=xyz. The route validates the token, checks the requester's access, and streams bytes from the STORAGE binding. Keeping the domain consistent has side effects you will appreciate later. Links never leak infrastructure details. You can attach per-request logic, things like download counting, expiry windows, or one-time links, without touching bucket policy. And if you ever migrate storage backends, every shared link keeps working because the URL contract belongs to your app, not to the bucket.
When Supabase is the stack #
R2 is the native path, tuned for SvelteKit on Cloudflare. But some projects live on Supabase, with Postgres as the database and Supabase Auth guarding the rows. For those, BYOB supports Supabase Storage as a distinct option instead of R2 bindings.
Supabase Storage is S3-compatible object storage with access control expressed through Postgres row-level security policies, as stated in the Supabase Storage docs (https://supabase.com/docs/guides/storage). When this path is active, the code generator loads Supabase-specific knowledge: client instantiation, bucket policies, and signed URL helpers in Supabase's shape rather than binding-shaped code.
Pick one path per project based on your database decision. R2 plus D1 keeps everything inside Cloudflare's runtime with binding-style access. Supabase Storage keeps files next to a Supabase database with policy-based access. Mixing both in one project is possible but rarely worth the confusion.
The table below picks the storage shape that fits your files.
| Option | Best use | Limit to respect |
|---|---|---|
| R2 bucket binding | Images and files inside your app | Route all access through server code |
| App signed URLs | Private sharing on your domain | Tokens must expire |
| Supabase storage | Projects already on Supabase | Keep one storage home per project |
| Public assets | Logos and open downloads | Never mix private files in |
| Server validation | Every upload path | Reject bad types and sizes early |
Tradeoffs worth naming #
The proxy pattern adds a hop. Direct-to-bucket uploads with presigned URLs can push bytes faster for very large files, since data skips your server code. BYOB accepts the small latency cost because the security posture is better and the code stays simpler. If you ever need resumable 5 GB uploads, that is the moment to revisit the decision with eyes open, using multipart upload through a dedicated route.
Bindings also tie you to Cloudflare's runtime. That is a fair trade while BYOB hosts your project, and the generated code is standard enough to adapt if you leave. The storage helper is a thin wrapper, so swapping the backend later means rewriting one module, not every route.
Ship the boring version first. A guarded dock beats a fast open door.
Manage your project storage ->
What we learned building this #
Uploads pass through SvelteKit server routes under src/routes that check the session before touching the STORAGE binding. The pattern matches the rest of the codebase: private work happens on the server, the browser only sees what it earned. Signed URLs keep sharing on your domain instead of an open bucket.
Who this is for (and who should skip it) #
This guide helps builders whose apps accept uploads, serve images, or share private files. If users hand you files that must stay safe and fast, the binding plus server route pattern above is your default.
Skip it if your site is static pages with a few bundled images. The built in asset pipeline already covers you, and object storage only pays off once uploads enter the picture.
- Best for developers adding user uploads and private file serving.
- Best for startups serving images fast with signed access.
- Best for small teams moving past static assets to real storage.