Eazip

EAZIP BLOG

4 Ways to Download Supabase Storage Files as a ZIP

Supabase Storage has no bulk ZIP download in the dashboard or API. The four approaches that work — the experimental CLI, an Edge Function or server ZIP, browser-side zipping with createSignedUrls, and a managed ZIP API — compared against the egress quota.

Ryan··4 min readsupabasezip

Supabase Storage serves files one object at a time. The dashboard has no "download folder", and neither storage-js nor the REST API has an endpoint that returns a prefix as an archive. Every "download all as ZIP" button on a Supabase app was built by its developers.

Two Supabase-specific facts shape which build is right: signing is unusually convenient (createSignedUrls signs a whole array in one call), and egress is a unified project quota — 5 GB/month on the Free plan, 250 GB included on Pro, then $0.09/GB — that database, API, and Storage all share. A ZIP feature is often the single largest consumer of that quota, so the pattern you pick shows up on the bill.

Which method should you use?

MethodBest forScale ceilingRuns on
CLI mirrorYou, one-offDisk spaceYour machine
Edge Function / server ZIPSmall archivesFunction memory & wall clockSupabase Edge / your server
Browser-side ZIPA "download all" button, no infraBrowser memory, tab lifetimeYour user's browser
Managed ZIP APIRecurring, multi-GB exportsPlan limits (up to 500 GB/job)A ZIP service

1. Mirror with the CLI, then zip locally

For a one-off pull of a bucket you own, the Supabase CLI's experimental storage commands can mirror a bucket to disk:

supabase storage cp -r ss:///your-bucket ./export --experimental
zip -r export.zip ./export

Where it stops working: it's experimental, it needs your project credentials and a terminal, and it will never be a button in your product.

2. Zip in an Edge Function or on your server

The DIY product path: download each object server-side (with the service role key) and stream an archive back. On your own Node server this is the standard archiver pattern; in an Edge Function the same idea runs under tighter memory and wall-clock budgets, which caps practical archive size fast.

The cost detail people miss: the files leave Supabase once per download — and if your server then serves the archive, the bytes cross your infrastructure too. Three downloads of a 5 GB export consume 15 GB of project egress quota, which is the entire Free-plan monthly allowance three times over.

Where it stops working: function limits first, then the quota math, then the usual streaming-ZIP operational issues (no Content-Length, so no progress bar and no resume).

3. Zip in the browser with createSignedUrls

Your endpoint authorizes the user, signs their files in one call, and the browser fetches and zips them locally with the open-source Eazip.js — no account, and no bytes through your server:

// Server: one call signs the whole folder
const { data } = await supabase.storage
  .from('uploads')
  .createSignedUrls(paths, 3600);

// Browser
import { createZip } from '@eazip/core';
const result = await createZip({
  files: signed.map((s) => ({ url: s.signedUrl, filename: s.path })),
  zipName: 'your-files.zip',
});
result.download();

Supabase's signed URLs respond with permissive CORS, so this path usually needs no storage configuration at all — one reason it's the most popular Supabase answer on this problem.

Where it stops working: browser memory and tab lifetime bound archive size, and each download re-fetches every object — repeat downloads keep drawing the egress quota.

4. Use a managed ZIP API

The same signed URL list, submitted server-to-server as one job. Eazip fetches the files from Supabase once, builds the archive, and returns an expiring download link:

const { data } = await supabase.storage
  .from('uploads')
  .createSignedUrls(paths, 3600);

await fetch('https://api.eazip.io/jobs', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.EAZIP_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    files: data.map((s) => ({ url: s.signedUrl, filename: s.path })),
    zip_filename: 'your-files.zip',
    expires_in: 172800,
  }),
});

For the quota, this is the prepare-once pattern: your Supabase egress is drawn exactly once per export, at preparation time, and every repeat download is served from Eazip's zero-egress storage without touching Supabase again. The full walkthrough — service role vs. RLS, listing pagination, the quota math — is in Zip Supabase Storage Files into a Download Link via API.

Where it stops working — honestly: it's a vendor dependency with metered volume beyond the free tier (100 files, 5 GB output per job). A small app whose exports fit comfortably in method 3 doesn't need it.

Choosing in one sentence each

  • It's for you, once: the experimental CLI mirror (method 1).
  • Small archives, no new dependencies: Edge Function or server ZIP (method 2) — watch the quota if downloads repeat.
  • A download-all button without infrastructure: createSignedUrls + browser ZIP (method 3).
  • Multi-GB, repeat downloads, or quota discipline: managed ZIP API (method 4) — egress drawn once per export.

FAQ

Can the Supabase dashboard download a folder?

No — single files only. Bulk download is always something you build.

Does zipping count against my Supabase egress quota?

Yes, whichever method: the bytes leave Supabase when they're fetched. The difference between methods is how many times — per download (methods 2 and 3) or once per export (method 4).

Do I need the service role key?

Only for backend signing that bypasses Row Level Security — and then your endpoint must authorize the request itself. A browser client signed in as the user can call createSignedUrls under that user's RLS policies instead.

What about buckets with more than 100 files?

storage.list() paginates (100 rows by default) — loop the offset when listing, whatever method you choose. Job size on the API path is plan-bound, up to 20,000 files.