Bring your own bucket
Bring your own bucket
Point a workspace at storage you already pay for, instead of hosted storage.
Available to every workspace, on any plan. Storage in your own bucket is unmetered — the plan’s storage limit only counts files on hosted storage.
Set up your bucket #
Point a workspace at a Cloudflare R2 bucket you already control. With your own bucket you:
- Keep files on storage you pay for and own directly — uploads.sh never holds a copy.
- Switch back to hosted storage any time, instantly and without touching existing files.
- See what’s different on a BYO bucket before you switch.
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). Any other S3-compatible provider works too — see Other S3-compatible providers.
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, a dedicated embed host serves badge-style no-cache headers that GitHub’s Camo
proxy revalidates, so an overwritten image refreshes in place. Give your own domain the same behavior
with one rule on your zone: Rules → Transform Rules → Modify Response Header, matching
your domain’s hostname, setting Cache-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 — a
GitHub embed can just keep showing the old bytes after an in-place overwrite.
The rule turns off edge caching for the whole domain — negligible for screenshot workflows (R2 egress is free), but worth weighing if the same bucket also 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 click Use this bucket.
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.
5. Optional: serve SVG and XML
By default, SVG and XML uploads 415 on a BYO bucket the same way they do everywhere else — served inline with no Worker in front, either format could otherwise run a script with your bucket’s origin. Set two headers on your bucket’s public host to open them up:
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandboxX-Content-Type-Options: nosniffTwo things decide whether the check passes:
- The headers must cover XML, not only SVG. A rule scoped to the
.svgextension leaves XML documents free to run script through an<?xml-stylesheet>XSLT. Scope yours by content type (image/svg+xml,application/xml,text/xml) or add the extensions. - Send exactly one
Content-Security-Policyheader. Two are legal CSP, but repeated headers arrive comma-joined and the check can’t read them apart, so it fails closed.
Then, on the workspace settings page, open the bucket’s Serve SVG and XML details and click Check now — it uploads an inert SVG and an inert XML file, fetches both back through your public URL, checks the headers on each, and deletes them. Both have to pass. A passing check is good for 30 days; after that (or if the check ever fails) SVG and XML go back to 415ing until you check again. Checks are limited to one a minute per bucket.
Other S3-compatible providers #
The connect form also takes any S3-compatible bucket — AWS S3, Backblaze B2, MinIO, and the rest. Pick Other S3-compatible on the form and it swaps the Cloudflare-only fields for:
- Endpoint URL — your provider’s S3 endpoint, for example
https://s3.us-east-1.amazonaws.com. Must behttps://. - Region — the bucket’s region, for example
us-east-1. - Bucket — typed in directly. There’s no bucket picker for S3 providers the way there is for R2; paste the name your provider gave you. AWS bucket names may contain dots.
Access keys work the same as the R2 walkthrough above. The public base URL does too, except the domain itself: step 3’s Cloudflare Custom Domain is specific to R2. For an S3-compatible provider, set up a public domain the way that provider (or a CDN in front of it) supports, then paste that URL into the form. The verify pipeline runs the same checks — shape, auth, a write/read/delete round-trip, and (for a first attach) that the bucket is empty — a public URL and the embed-cache rule are still recommended, not required. There’s no jurisdiction concept for S3 providers.
The embed-cache tip above is written for Cloudflare’s Transform Rules. On another provider, add the
CDN-equivalent no-cache rule on the public domain instead — set Cache-Control to
max-age=0, no-cache, no-store, must-revalidate for that host so GitHub’s Camo proxy revalidates
overwritten images.
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. |
| — | Not supported. |
| Files uploaded before switching | Keep serving from wherever they already were. | No migration, no broken links. Switching a workspace’s active bucket only changes where new 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 your provider 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.