EAZIP BLOG
3 Ways to Download All Files From Supabase Storage as a ZIP
Supabase Storage has no built-in ZIP export. Compare downloading files one by one, zipping on a server, and zipping in the browser with a streaming library.
Supabase Storage is an S3-compatible object store: buckets, folders, and
signed URLs. What it does not have is a "download this folder as one ZIP"
button, or an API endpoint that returns one. If your app has a
project-files bucket and a user asks to download all of it, you have to
build that yourself.
Search "supabase storage download folder" or "supabase bulk download" and you'll find people solving this three different ways, with different tradeoffs depending on folder size and how much backend you're willing to run. This post compares them honestly, including where each one breaks.
The three approaches at a glance
| Approach | Works for | Breaks at | Server needed | Effort |
|---|---|---|---|---|
| Download files one by one | A handful of files | ~10+ files (UX), browser download throttling | No | Low |
| Zip on a server (Edge Function or backend) | Small–medium folders | Function memory/CPU/timeout limits on large folders | Yes | Medium |
| Zip in the browser (streaming library) | Small to very large folders | Very large jobs need offloading to a worker/cloud step | Only to sign URLs | Low–medium |
All three start the same way: you need a list of the objects in the folder
and a way to read their bytes without exposing your service role key to the
browser. supabase.storage.from(bucket).list(prefix) gets the list;
createSignedUrls(paths, expiresIn) turns a batch of paths into time-limited
URLs you can hand to the client. list() defaults to 100 results per call,
so paginate with limit/offset if a folder can hold more than one page.
Way 1: Download files one by one
The simplest thing that works: loop over the objects and either call
supabase-js's download() for each one, or render a signed-URL link per
file and let the browser handle it.
const { data: objects } = await supabase.storage
.from('project-files')
.list(folder, { limit: 1000 });
for (const object of objects ?? []) {
const path = `${folder}/${object.name}`;
const { data: blob } = await supabase.storage
.from('project-files')
.download(path);
if (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = object.name;
a.click();
URL.revokeObjectURL(url);
}
}Verdict: fine for two or three files. Past that it falls apart fast. Browsers throttle or block multiple near-simultaneous downloads triggered by script — Chrome will prompt the user to allow multiple downloads after the first few, and some browsers cap concurrent downloads outright. Even when it works, the user gets a folder full of loose files instead of the one archive they asked for, and has to click through a permission prompt per file on some setups. This approach doesn't scale past roughly ten files, and it was never really a "download all" feature — it's a loop that happens to work for a small number of files.
Way 2: Zip on a server
Move the work server-side: fetch each object (or read it directly with the service role key), pipe the bytes into an archiving library, and return the finished ZIP. A Supabase Edge Function is the natural place to put this if you don't already run a backend.
// supabase/functions/export-zip/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2';
// Deno equivalent of a streaming zip writer, e.g. a Deno-compatible
// zip library that can accept ReadableStreams per entry.
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
const { folder } = await req.json();
const { data: objects } = await supabase.storage
.from('project-files')
.list(folder, { limit: 1000 });
// stream each object into the zip writer, then stream the
// finished archive back as the function response
// ...
return new Response(zipStream, {
headers: { 'Content-Type': 'application/zip' },
});
});Verdict: this works, and it keeps the archive assembly off the client entirely — useful if you don't want any bucket bytes touching the browser. But Supabase Edge Functions run under real constraints: 256 MB of memory per invocation, a 2-second CPU time budget (async I/O like fetching objects doesn't count against that, but archive compression does), and a wall-clock limit of 150 seconds on the free plan or 400 seconds on paid plans before the function is killed. A folder of a few dozen small files zips fine inside those limits. A folder of a few thousand files, or one with several gigabytes total, can blow through memory if you buffer entries instead of streaming them, or simply run out of wall-clock time before the archive finishes. If you run this on your own backend instead of an Edge Function you get to pick your own limits, but then you own the scaling, the disk or memory sizing for concurrent export requests, and the egress cost of both reading from Supabase Storage and serving the ZIP back out — twice through your infrastructure instead of once.
Way 3: Zip in the browser with a streaming library
The third option skips the server-side archive step. The server still signs URLs — it never hands out the service role key — but the browser fetches each object and writes the ZIP itself, streaming entries in rather than holding the whole archive in memory at once. Eazip is one library built for this.
import { createZip } from '@eazip/core';
const response = await fetch(`/api/exports/files?folder=${folder}`);
const files = await response.json(); // [{ url, filename }, ...] signed URLs
const result = await createZip({
files,
zipName: 'project-files.zip',
});
result.download();Verdict: no zip server to run or scale, and the streaming writer means
tab memory doesn't grow linearly with archive size the way a naive
in-memory approach does. A common DIY version of this pattern uses
JSZip directly: fetch every file, hold all
of the resulting blobs in memory, then call generateAsync() to produce the
archive. That's a reasonable choice for a small export, but because it
builds the whole ZIP in memory before writing anything out, a folder in the
gigabyte range can exhaust tab memory or freeze the UI before it finishes —
which is the specific problem a streaming writer avoids.
Be honest about what this approach costs you, too: the browser is now doing the fetching, so Supabase Storage's CORS configuration has to allow your app's origin, or the signed-URL fetches fail. You're also adding a third-party dependency to the client bundle instead of keeping the archive logic entirely server-side. And a browser tab is still a browser tab — for very large exports (multi-gigabyte, thousands of files, or a job that should survive a reload), Eazip's Cloud mode moves the same fetch-and-zip work to a hosted worker instead of the tab, at the cost of routing those signed-URL reads through a third party rather than directly from Supabase to the browser.
Which one to pick
- A handful of files, no urgency: per-file signed URL links. No backend work beyond signing.
- You already run a backend and folders stay small-to-medium: zip on the server. Straightforward, and the client never sees individual object URLs.
- You want "download all" without operating a zip server, and folders can get large: zip in the browser with a streaming library, moving to a cloud/worker step once folders are big enough to strain tab memory or a tab's lifetime.
If you're building the browser-streaming version against Supabase specifically, the full walkthrough — including folder pagination past the first 100 objects and the exact CORS configuration Supabase Storage expects — is in Download All Files From Supabase Storage as a ZIP. For the point where a folder outgrows the browser, see when to use Eazip Cloud.
FAQ
How do I download all files from Supabase Storage at once?
There's no single API call for it. List the objects with list(), get
either per-file signed URLs or their bytes, and combine them into one
archive — either on a server or in the browser. See the three approaches
above for the tradeoffs of each.
Does Supabase Storage support downloading a folder as a ZIP?
No. Supabase Storage serves individual objects; folders in the storage UI
are a naming convention (prefix/name), not a real container you can
request as an archive. You always assemble the ZIP yourself, client-side or
server-side.
What's the best way to bulk download from Supabase Storage without running my own zip server?
Sign the objects' URLs on the server, then build the ZIP in the browser with a streaming library like Eazip so tab memory doesn't grow with archive size. It's the middle ground between per-file links (bad UX past a handful of files) and running your own zip backend (works, but you own its scaling and limits).
Can a Supabase Edge Function zip a large folder?
It can, up to its resource limits: 256 MB memory, a 2-second CPU budget, and a wall-clock limit of 150 seconds on the free plan or 400 seconds on paid plans. A folder of a few dozen small files is comfortably inside those limits; a folder of thousands of files or several gigabytes total risks hitting memory or the wall-clock timeout before the archive finishes, depending on how the function streams (or doesn't stream) the zip writer.