Skip to content

file storage · side by side

Cloudflare R2 vs Supabase Storage for a Next.js app

Both fill the file storage slot, so a generated repo carries one or the other, never both. Every line below is read out of the two manifests.

Short answer

Pick Cloudflare R2 if

Anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again.

Pick Supabase Storage if

Teams already on Supabase who want files under the same policies as their rows. Same migrations, same pull request. Strongest for private per-user files (avatars, invoices, imports) where "the owner and nobody else" is the whole access model.

Cloudflare R2 is tested with Neon. Supabase Storage isn't yet.

Side by side

Price, obligations, and the surface each one adds. No row is written by hand. This is manifest.yaml, rendered.

Cloudflare R2 compared with Supabase Storage on pricing, fit, trade-offs, required companions, environment variables, dependencies, MCP servers and repo footprint.
From the manifestOption ACloudflare R2Option BSupabase Storage
In one lineCloudflare R2

S3-compatible object storage with free egress. You pay for storage and requests.

Supabase Storage

Object storage in your Supabase project, with the same row level security as tables.

PricingCloudflare R2

Free each month: 10 GB stored, 1M Class A and 10M Class B operations. Then $0.015 per GB-month, $4.50 per million Class A operations (writes, lists) and $0.36 per million Class B operations (reads). Egress is free.

Supabase Storage

Part of your Supabase plan. Free: 1 GB stored and 5 GB egress. Pro: 100 GB stored, then $0.0213/GB, and 250 GB egress, then $0.09/GB ($0.03/GB for cached egress). Image transformations need Pro: 100 origin images included, then $5 per 1,000.

Best forCloudflare R2

Anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again.

Supabase Storage

Teams already on Supabase who want files under the same policies as their rows. Same migrations, same pull request. Strongest for private per-user files (avatars, invoices, imports) where "the owner and nobody else" is the whole access model.

Trade-offsVerbatim from the manifestCloudflare R2
  • No egress fee, but operations are billed. Many tiny reads can cost more than the bytes. Check Class B pricing against your access pattern, not just your storage volume.
  • S3-compatible, not S3. The common surface works with the AWS SDK. Parts of the long tail (some checksum modes, ACLs, object lock, storage classes) do not. Test, do not assume.
  • Authorisation is your job. R2 has no row level security, so "may this user read this key" is a decision your app makes on every request.
  • A bucket is private until you attach a custom domain or enable the r2.dev subdomain, and r2.dev is rate-limited and not for production. Public serving means DNS work, not a checkbox.
  • Strong read-after-write consistency, but no built-in image transforms. That is a separate Cloudflare product, or a resize on upload in your own code.
  • Bucket config (CORS, lifecycle) lives in Cloudflare, not in your migrations. It needs its own committed files and a command to apply them. This battery ships both.
Supabase Storage
  • Egress is billed. If you serve large media at volume, the transfer line will outgrow the storage line. That is the case R2 exists for.
  • Policies are SQL against storage.objects, so the key layout is part of your security model. Change the shape of your keys and every policy changes with it.
  • The service role key bypasses every policy. Server code that uses it does its own authorisation, and the RLS policies only guard what holds a user token.
  • Image transformation is convenient and metered per origin image. Cheap for avatars, surprising for a gallery.
  • Signed upload URLs last two hours and that cannot be shortened, so treat the URL itself as a credential.
  • One bucket per access model, not per feature. Public and private objects in one bucket end in leaked files or a pile of policy exceptions.
Required companionsAdded for you, with a reasonCloudflare R2

Nothing. It stands on its own.

Supabase Storage
  • Supabase
Recommended alongsideSuggested, never added for youCloudflare R2
  • An auth battery
Supabase Storage
  • An auth battery
Env vars you will manageEvery one documented in docs/onboard.mdCloudflare R2

7 variables · 4 required

  • R2_ACCESS_KEY_ID
  • R2_ACCOUNT_ID
  • R2_BUCKET
  • R2_SECRET_ACCESS_KEY
  • R2_PUBLIC_BASE_URL
  • STORAGE_ALLOWED_ORIGINS
  • STORAGE_DEV_UPLOADER
Supabase Storage

3 variables · 1 required

  • SUPABASE_STORAGE_BUCKET
  • STORAGE_ALLOWED_ORIGINS
  • STORAGE_DEV_UPLOADER
Dependencies addedCloudflare R2
  • @aws-sdk/client-s3 ^3.1141.0
  • @aws-sdk/s3-request-presigner ^3.1141.0
  • server-only ^0.0.1
Supabase Storage
  • @supabase/supabase-js ^2.117.0
  • server-only ^0.0.1
MCP serversWritten into .mcp.jsonCloudflare R2

None. No extra agent tools from this one.

Supabase Storage

None. No extra agent tools from this one.

Footprint in your repoCloudflare R2

13 files, plus 3 injections into shared stack files

Supabase Storage

12 files, plus 3 injections into shared stack files

What changes in your repo

The paths each battery contributes, diffed. A path in the third list is written by both, so swapping rewrites that file rather than adding one.

Only with Cloudflare R2 (3)

  • infra/2 files
    • r2/2 files
      • cors.json
      • lifecycle.json
  • scripts/1 file
    • r2/1 file
      • apply-config.ts

Only with Supabase Storage (2)

  • supabase/1 file
    • migrations/1 file
      • 20250101000200_storage_bucket.sql
  • variants/1 file
    • auth-supabase/1 file
      • supabase/1 file
        • migrations/1 file
          • 20250101000201_storage_policies.sql

Same path, different implementation (10)

  • src/5 files
    • app/1 file
      • api/1 file
        • upload/1 file
          • route.ts
    • components/1 file
      • upload/1 file
        • file-dropzone.tsx
    • lib/3 files
      • storage/3 files
        • authorize.ts
        • index.ts
        • keys.ts
  • tests/1 file
    • unit/1 file
      • storage-keys.test.ts
  • variants/4 files
    • auth-none/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • uploader.ts
    • auth-wired/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • uploader.ts
    • errors-none/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • report.ts
    • errors-sentry/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • report.ts

Shared stack files Cloudflare R2 injects into

  • env-required
  • legal-processors
  • verify-checks

Shared stack files Supabase Storage injects into

  • env-required
  • legal-processors
  • verify-checks

Cloudflare R2 in your .env.local

.env.localbash10 lines
# required
R2_ACCESS_KEY_ID=0123456789abcdef0123456789abcdef
R2_ACCOUNT_ID=0123456789abcdef0123456789abcdef
R2_BUCKET=uploads
R2_SECRET_ACCESS_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

# optional
R2_PUBLIC_BASE_URL=https://files.example.com
STORAGE_ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com
STORAGE_DEV_UPLOADER=dev-user

Supabase Storage in your .env.local

.env.localbash6 lines
# required
SUPABASE_STORAGE_BUCKET=uploads

# optional
STORAGE_ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com
STORAGE_DEV_UPLOADER=dev-user

What changes for your agents

Each battery ships rules, skills, subagents and hooks that an agent loads before it touches the code that battery owns. Picking one is also picking how your agents behave in src/lib/storage/**.

Cloudflare R2

  • 2

    Skills

  • 2

    Rules

  • 5

    Solution docs

Rules (2)

Bucket configuration is code, and R2 is S3-compatible rather than S3

infra/r2/** · scripts/r2/** · src/lib/storage/**

Never sign a URL without an authorisation check, and never proxy uploads through a route

src/lib/storage/** · src/app/api/upload/** · src/components/upload/**

Skills (2)

/add-bucket

Add a second R2 bucket for a different access model, with its CORS and lifecycle configuration committed as code and applied by script rather than clicked into the dashboard.

/upload-flow

Add a complete file upload to a feature: authorised signing route, direct-to-R2 PUT, the database row that records the key, and the cleanup that stops orphans.

Subagents and hooks

None of its own. The foundation agents and guard hooks still ship.

Supabase Storage

  • 2

    Skills

  • 2

    Rules

  • 5

    Solution docs

Rules (2)

Buckets and their policies are migrations, not console clicks

supabase/migrations/** · src/lib/storage/**

Never sign a URL without an authorisation check, and never proxy uploads

src/lib/storage/** · src/app/api/upload/** · src/components/upload/**

Skills (2)

/add-bucket-policy

Add or change a Supabase Storage bucket and its row level security policies as a migration, then prove the policy actually denies what it should.

/upload-flow

Add a complete file upload to a feature: authorised signing route, direct-to-storage upload, the database row that records the key, and the cleanup that stops orphans.

Subagents and hooks

None of its own. The foundation agents and guard hooks still ship.

What each one already knows

Solution docs land in docs/solutions/ in your repo and are published here, so you can read the failure modes before you commit.

Cloudflare R2 (5)

Supabase Storage (5)

Which one to pick

From meta.bestFor and meta.tradeoffs. If a claim is not in the manifest, it is not on this page.

Pick Cloudflare R2 when

Anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again.

And accept that(6)
  • No egress fee, but operations are billed. Many tiny reads can cost more than the bytes. Check Class B pricing against your access pattern, not just your storage volume.
  • S3-compatible, not S3. The common surface works with the AWS SDK. Parts of the long tail (some checksum modes, ACLs, object lock, storage classes) do not. Test, do not assume.
  • Authorisation is your job. R2 has no row level security, so "may this user read this key" is a decision your app makes on every request.
  • A bucket is private until you attach a custom domain or enable the r2.dev subdomain, and r2.dev is rate-limited and not for production. Public serving means DNS work, not a checkbox.
  • Strong read-after-write consistency, but no built-in image transforms. That is a separate Cloudflare product, or a resize on upload in your own code.
  • Bucket config (CORS, lifecycle) lives in Cloudflare, not in your migrations. It needs its own committed files and a command to apply them. This battery ships both.

Pick Supabase Storage when

Teams already on Supabase who want files under the same policies as their rows. Same migrations, same pull request. Strongest for private per-user files (avatars, invoices, imports) where "the owner and nobody else" is the whole access model.

And accept that(6)
  • Egress is billed. If you serve large media at volume, the transfer line will outgrow the storage line. That is the case R2 exists for.
  • Policies are SQL against storage.objects, so the key layout is part of your security model. Change the shape of your keys and every policy changes with it.
  • The service role key bypasses every policy. Server code that uses it does its own authorisation, and the RLS policies only guard what holds a user token.
  • Image transformation is convenient and metered per origin image. Cheap for avatars, surprising for a gallery.
  • Signed upload URLs last two hours and that cannot be shortened, so treat the URL itself as a credential.
  • One bucket per access model, not per feature. Public and private objects in one bucket end in leaked files or a pile of policy exceptions.

Questions people actually ask

Should I choose Cloudflare R2 or Supabase Storage?
Cloudflare R2 is best for anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again. Supabase Storage is best for teams already on Supabase who want files under the same policies as their rows. Same migrations, same pull request. Strongest for private per-user files (avatars, invoices, imports) where "the owner and nobody else" is the whole access model. Both fill the file storage slot, so a generated repo carries one or the other, never both.
How much do Cloudflare R2 and Supabase Storage cost?
Cloudflare R2: Free each month: 10 GB stored, 1M Class A and 10M Class B operations. Then $0.015 per GB-month, $4.50 per million Class A operations (writes, lists) and $0.36 per million Class B operations (reads). Egress is free. Supabase Storage: Part of your Supabase plan. Free: 1 GB stored and 5 GB egress. Pro: 100 GB stored, then $0.0213/GB, and 250 GB egress, then $0.09/GB ($0.03/GB for cached egress). Image transformations need Pro: 100 origin images included, then $5 per 1,000.
What changes in my repo if I switch from Cloudflare R2 to Supabase Storage?
Cloudflare R2 writes 13 files, 7 environment variables and 3 dependencies, and installs 2 path-scoped rules, 2 skills and 5 solution docs. Supabase Storage writes 12 files, 3 environment variables and 2 dependencies, and installs 2 path-scoped rules, 2 skills and 5 solution docs.

Decide once, then build the repo that already knows the decision.

Either way you get that choice’s rules, skills and solution docs installed, plus the guard hooks, an onboarding doc for exactly these env vars, and the Compound Engineering loop. Free and MIT.