Bring your own bucket
Bring your own bucket
Point a workspace at storage you already pay for, instead of hosted storage.
Set up your bucket #
A workspace admin connects from the workspace settings page — one form, filled from three things you set up on the Cloudflare dashboard first (plus one optional cache rule).
1. Create an R2 bucket
Dashboard → R2 → Create bucket. Any name that follows Cloudflare's rules works (3–63 characters; lowercase letters, digits, hyphens). Jurisdiction buckets (eu, fedramp) work too — it's detected automatically from the endpoint URL, or probed during verification.
2. Create a bucket-scoped API token
R2 → Manage API Tokens → Create API Token, permission Object Read & Write, scoped to that one bucket. Copy the Access Key ID and Secret Access Key — and either your account ID or the S3 endpoint URL shown on the same screen. The form takes either one, and once your keys are in it looks up your buckets for you where the token allows it.
Scope the token to one bucket, not the whole account. uploads.sh encrypts the key pair, but a bucket-scoped token still limits the blast radius if anything goes wrong.
3. Give the bucket a domain
Bucket → Settings → Public access → Custom Domains, connect a domain on a Cloudflare zone in your account, then paste the URL (for example https://media.example.com) into the form as the public base URL. The settings page requires one — it's what makes links stable and embeddable. See the serving matrix below.
4. Optional but recommended: add a cache rule for GitHub embeds
On hosted storage, images embedded in GitHub comments refresh when a file is overwritten in place, because a dedicated embed host serves badge-style no-cache headers that GitHub's Camo proxy revalidates. You can give your own domain the same behavior with one rule on your zone:Rules → Transform Rules →Modify Response Header, matching your domain's hostname, settingCache-Control to max-age=0, no-cache, no-store, must-revalidate. Verification checks for it and reminds you if it's missing — without it everything still works, but a GitHub embed can keep showing the old bytes after an in-place overwrite. The trade-off: these headers turn off edge caching for the whole domain, which is negligible for screenshot workflows (R2 egress is free) but worth weighing if the same bucket serves high-traffic assets — in that case, connect a second custom domain to the bucket and scope the rule to just that host.
Then Verify & save: one click checks the settings, signs in to the bucket, round-trips a test object, and fetches it through your public URL. Anything that fails is spelled out on the form; a clean pass saves. Saving never changes where uploads go — the bucket sits on your settings page as "Not in use yet" until you clickUse this bucket, so you can connect and verify at any time without any risk to what's already uploading.
Switching is instant and reversible, even for a workspace that already has files: existing files keep resolving from wherever they already are, and only new uploads move to the newly active bucket. File-size and video-size limits still apply on a BYO bucket; the storage-quota budget does not.
Serving: custom domain vs signed-only #
| Public access | How files are served | Trade-off |
|---|---|---|
| Custom domain | Direct public URL under your domain. | Recommended — needs a domain on a Cloudflare zone in your account. |
| No public URL | Signed URLs, generated on demand. | Degraded — links expire after an hour and GitHub embeds won't render (see below). The settings page requires a public base URL; signed-only configs can only be saved through the API. |
r2.dev managed URL | — | Not supported.r2.dev base URLs are rejected — connect a custom domain instead. |
| Files uploaded before switching | Keep serving from wherever they already were. | No migration, no broken links. Switching a workspace's active bucket only changes wherenew uploads land — existing files and their published URLs don't move. |
What's different #
- GitHub embeds. Hosted-storage images get a dedicated embed host that GitHub's Camo proxy revalidates after an in-place overwrite. On your own domain you get the same behavior by adding the cache rule above — without it, embeds still render but may show stale bytes after an overwrite. A BYO image with no public URL at all (signed-only) won't render inline in a GitHub comment; the file still uploads and still has a share page.
retentionDaysauto-cleanup. Age-based retention walks a prefix on the hosted bucket. A BYO bucket has no hosted prefix, so automatic retention isn't available — delete old files yourself if you need that.- Storage quota. On a BYO bucket the plan's storage budget doesn't apply (you pay Cloudflare for the bucket). File-size and video-size upload limits still do.
Data ownership & switching back #
The bucket and everything in it are yours. uploads.sh stores only encrypted credentials and a pointer to the bucket — never a copy of your files.
Switching back to hosted storage never touches your objects. Files you uploaded while your bucket was active keep resolving from it — only new uploads move to hosted storage. Switching to your bucket again later is the same instant, reversible switch in the other direction.