Download All Files From Supabase Storage as a ZIP
List a Supabase Storage folder, sign the objects on the server, and package them into one ZIP in the browser with Eazip.
Supabase Storage has no built-in "download folder as ZIP" action. To let a user download every file in a bucket or folder as one archive, list the objects, create signed URLs on the server, and hand that list to Eazip in the browser to fetch and package. The server never exposes your service role key or bucket credentials to the client.
Loading live demo…
Prepare signed URLs on the server
Your route handler should:
- authenticate the caller and confirm they may access the folder;
- list the objects with
supabase.storage.from(bucket).list(prefix); - sign the matching paths with
createSignedUrls(paths, expiresIn); and - return only
{ url, filename }entries.
// app/api/exports/files/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const BUCKET = 'project-files';
const SIGNED_URL_TTL = 60 * 10; // 10 minutes
export async function GET(request: Request) {
const folder = new URL(request.url).searchParams.get('folder') ?? '';
// TODO: authenticate the caller and verify they may access `folder`.
const { data: objects, error: listError } = await supabase.storage
.from(BUCKET)
.list(folder, {
limit: 1000,
sortBy: { column: 'name', order: 'asc' },
});
if (listError) {
return NextResponse.json({ error: listError.message }, { status: 500 });
}
// list() also returns a placeholder entry for each subfolder; those have
// no id and no signable object behind them.
const paths = objects
.filter((object) => object.id !== null)
.map((object) => `${folder}/${object.name}`);
const { data: signed, error: signError } = await supabase.storage
.from(BUCKET)
.createSignedUrls(paths, SIGNED_URL_TTL);
if (signError) {
return NextResponse.json({ error: signError.message }, { status: 500 });
}
const files = signed
.filter((entry) => !entry.error && entry.signedUrl)
.map((entry) => ({
url: entry.signedUrl,
filename: entry.path!.split('/').pop()!,
}));
return NextResponse.json(files);
}Use the service role key only on the server. list() defaults to 100 results
per call and accepts limit/offset, so paginate through the folder before
signing if it can hold more than one page of files.
Create the ZIP in the browser
import { createZip } from '@eazip/core';
const response = await fetch(`/api/exports/files?folder=${folder}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Could not prepare export files');
}
const files = await response.json();
const result = await createZip({
files,
zipName: 'project-files.zip',
});
result.download();files here is the { url, filename }[] array your route returned; Eazip
also accepts plain URL strings, File, FileList, and Blob in the same
array, so it fits alongside inputs the user picked locally. See
Input types for the full list of accepted shapes.
For a Local job, each signed URL must stay valid until Eazip fetches it, and the request must succeed as a browser CORS fetch. If your project's storage endpoint rejects the cross-origin request, or you would rather not depend on it, sign the URLs the same way and switch straight to Cloud below — the code in this section does not otherwise change. This is the same pattern used to create a ZIP from remote URLs in general.
When to use Eazip Cloud
Local is enough for a typical export folder. Reach for Eazip Cloud when at least one of these is true:
- the folder adds up to a multi-gigabyte archive, or holds thousands of files;
- the storage endpoint blocks the browser's CORS request;
- the export should keep running if the user reloads or closes the tab; or
- you don't want archive bytes sitting in tab memory.
The signing endpoint above stays the same. Only the createZip call changes:
const result = await createZip({
strategy: 'cloud',
publicKey: 'pk_ez_...',
files,
zipName: 'project-files.zip',
});
result.downloadAll();Add your Supabase project's storage hostname
(<project-ref>.supabase.co) to the Public App's allowed source hosts, and
keep SIGNED_URL_TTL long enough for Eazip Cloud to start and finish
reading every object. Cloud runs in stream mode by default; switch to stored
mode if you need a reusable download link instead of a one-time stream. Cloud
usage is metered, and Supabase may still charge its own egress for the
signed-URL fetches Cloud performs on your behalf.
Limits and alternatives
Eazip is overkill for a handful of small public files — link directly to each object's public URL, or to a single signed URL, and skip the ZIP step.
For everything larger, consider what the naive approaches cost:
- A link per file works but forces the user to click and save each download individually, which most users abandon past a handful of files.
supabase-jsdownload()plus a client-side zip library fetches every object's bytes into memory before it can write the archive. That works for a small export, but a large folder can exhaust tab memory or freeze the UI before the ZIP finishes — the same constraint Eazip's Local mode manages for you, and the reason Cloud exists for larger exports.- Supabase's own free-tier egress allowance is limited, so an export flow that repeatedly re-downloads the same large files can hit that cap faster than expected; check your project's current plan limits before shipping a bulk-export feature.
FAQ
Can I download an entire Supabase Storage bucket as a ZIP?
Yes. List every object with list() (paginating past the first 100 results
if needed), sign the paths with createSignedUrls(), and pass the resulting
{ url, filename } list to createZip(). Use Cloud instead of Local if the
bucket is large enough to strain browser memory or a tab lifetime.
Does Supabase have a built-in ZIP download button?
No. Supabase Storage serves individual objects; it has no server-side endpoint that returns a folder as a ZIP. You assemble the archive on the client with a library like Eazip, or run that step in Eazip Cloud.
Do I need to expose my Supabase service role key to the browser?
No, and you should not. Keep the service role key on the server. The route handler in this guide uses it to list and sign objects, then returns only short-lived signed URLs and filenames to the browser.
Why not just fetch and zip the files with supabase-js in the browser?
You can for a small export, but download() pulls each object's full bytes
into memory before a client-side zip library can write them out, so a large
folder risks running out of tab memory. Eazip streams objects into the
archive and can move the work to Cloud once local limits are a concern.
What if my signed URLs fail with a CORS error?
Confirm the storage endpoint allows your app's origin for the browser fetch.
If it doesn't, or you'd rather not rely on it, keep the same signing code and
switch createZip to strategy: 'cloud' — Cloud fetches the signed URLs
from Eazip's servers instead of the browser, so the client never needs CORS
access to Supabase Storage.
Zip Thousands of URLs with JavaScript
Package thousands of remote URLs with JavaScript and Eazip Cloud, with React code, partial results, and resumable job state.
Download an Entire S3 Bucket or Prefix as a ZIP
List a bucket prefix, presign each object, and package the results into one ZIP with @eazip/core in the browser.