Eazip.js
EazipGitHub

Create a ZIP From Presigned URLs

Turn any storage provider's expiring signed URLs into one browser ZIP download with @eazip/core, without exposing credentials.

Any storage that issues expiring, signed URLs works the same way: your server authenticates the request and signs each object, and the browser fetches the resulting URLs and zips them with @eazip/core. This pattern applies whether the URLs come from Amazon S3, an S3-compatible provider, or a completely different object store — the only thing that changes is how you generate the URL.

Loading live demo…

Pick your path

Where the signed URLs already live decides how much of this guide you need. The main path below assumes your server has to mint them. If that isn't your situation, the full decision guide covers every option.

Your situationPath
The browser already holds expiring URLsSkip the server step and pass them to createZip directly
Your server must mint the signed URLsSigning endpoint plus a browser ZIP — this guide's main path, below
Large or private exports on CloudA backend-created session, see Other setups

The pattern

Every provider that supports presigned or signed URLs needs the same four server-side responsibilities:

  1. authenticate the application user;
  2. authorize the specific objects being requested;
  3. create short-lived signed URLs; and
  4. return only { url, filename } entries — never storage credentials.
server/export.ts
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  region: 'auto',
  endpoint: process.env.STORAGE_ENDPOINT,
});

type SignedSource = {
  url: string;
  filename: string;
};

export async function signSources(keys: string[]): Promise<SignedSource[]> {
  return Promise.all(
    keys.map(async (key) => ({
      url: await getSignedUrl(
        s3,
        new GetObjectCommand({ Bucket: process.env.BUCKET, Key: key }),
        { expiresIn: 900 },
      ),
      filename: key,
    })),
  );
}

Only the endpoint (and sometimes region) changes between providers, because @aws-sdk/s3-request-presigner works against any S3-compatible API. That covers Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Wasabi, MinIO, Hetzner Object Storage, Scaleway Object Storage, OVHcloud Object Storage, and Tigris. Providers with their own SDK — Supabase Storage, Firebase Storage, Azure Blob Storage — use a different signing call, but return the same { url, filename } shape.

Create the ZIP in the browser

Once your endpoint returns signed sources, fetching and zipping them is provider-agnostic:

download-export.ts
import { createZip } from '@eazip/core';

const response = await fetch('/api/exports/123/files', {
  credentials: 'include',
});

if (!response.ok) {
  throw new Error('Could not prepare export files');
}

const files: { url: string; filename: string }[] = await response.json();

const result = await createZip({
  files,
  zipName: 'export.zip',
});

result.download();

createZip() resolves once every reachable URL has been fetched and packaged; result.download() then starts the browser download. files also accepts plain URL strings or a mix of URLs and browser File/Blob objects — see Input types for every accepted shape. For a Local job, the storage provider must allow browser CORS on GET requests from your application's origin, or Eazip reports each blocked file as failed.

Pick the URL lifetime

The signed URL's expiry has to outlive the ZIP job, not just the moment the link is generated:

  • Small jobs (a handful of files) can use a short expiry, such as the 15-minute default shown above — the whole fetch usually finishes in seconds.
  • Large jobs (many files, or large files on a slow connection) need a longer expiry, because the last file is signed once but fetched only after every earlier file has downloaded.
  • A URL that expires mid-job fails only that one file. With failOnUrlError left at its default, createZip() still returns a partial ZIP with the files that succeeded; set failOnUrlError: true if every file is required and a failure should reject the whole job instead.

When in doubt, sign for longer than you think you need. A signed URL that sits unused for a few extra minutes costs nothing; one that expires before the fetch runs turns into a support ticket.

When to use Eazip Cloud

Local jobs run entirely in the visitor's browser tab and cover most everyday downloads. Switch to strategy: 'cloud' when:

  • the archive is multi-gigabyte or contains thousands of signed URLs;
  • the storage provider can't or won't allow browser CORS;
  • the job needs to survive a page reload; or
  • you'd rather not hold archive bytes in tab memory at all.
const result = await createZip({
  strategy: 'cloud',
  publicKey: 'pk_ez_...',
  files,
  zipName: 'export.zip',
});

result.downloadAll();

Cloud fetches the same signed URLs outside the browser, so add the storage provider's hostname to the Public App's allowed source hosts. Stream mode is the default and adds no separate outbound-bandwidth fee on top of Eazip's own metered usage, though the storage provider may still charge for the source fetch; stored mode instead keeps a reusable archive artifact. See When to use Eazip Cloud for the full comparison with Local.

Other setups

Two variations replace the signing endpoint entirely.

  • Backend-created session — for exports that are large, or whose source list shouldn't travel to the browser at all: a 1,000-file export otherwise means signing and shipping 1,000 URLs in one response. With createSession, your endpoint returns only { sessionId, clientSecret } and the URL list never crosses the network. See Backend-created sessions.
  • No browser involved — scheduled or automated exports, such as a nightly archive of a bucket, call the HTTP API directly with a secret key and receive a webhook when the ZIP is ready. No frontend SDK takes part.

FAQ

How long should presigned URLs live for a ZIP job?

Long enough to outlast the whole job, not just the request that creates them. Size the expiry to your slowest expected download, and remember the last file in the list is fetched only after every earlier one — so a large job needs a longer expiry than a single-file download link.

Do presigned URLs need CORS for browser downloads?

Yes, for Local jobs. The browser fetches each signed URL directly from the storage provider, so the provider must return Access-Control-Allow-Origin for your application's origin on GET requests. Eazip Cloud fetches outside the browser and isn't subject to browser CORS.

Can I zip presigned URLs from any S3-compatible provider the same way?

Yes. @aws-sdk/s3-request-presigner and the AWS SDK's S3Client work against any S3-compatible endpoint, including Hetzner Object Storage, Scaleway Object Storage, OVHcloud Object Storage, and Tigris — point endpoint at the provider and the rest of the signing and zipping code stays the same.

What happens if one signed URL expires before Eazip fetches it?

By default, only that file is skipped; createZip() still resolves with a partial ZIP containing the files that succeeded, and result.status is 'partial'. Set failOnUrlError: true if the job should fail outright instead.

Do I need Eazip Cloud just to use presigned URLs?

No. Presigned URLs work with Local by default — Cloud is only needed once the job outgrows the browser tab, as described above.

Next steps