Add "Download All" to a Client Portal
Give portal users one button that turns their documents into a ZIP — authorized per client, built as a background job, and delivered as an expiring link that survives the page.
Client portals accumulate files — contracts, deliverables, statements, project assets — and sooner or later a client asks for the button that downloads all of them at once. This guide adds that button the way a portal needs it built: authorized per client, resilient to large folders, and never blocked by the user closing the tab.
The portal-specific constraints:
- Authorization is the feature. Every export must be scoped to exactly the requesting client's files. The ZIP layer must not widen access.
- Folder sizes are unpredictable. One client has 30 PDFs; another has four years of deliverables. The same button must handle both.
- Users navigate away. A synchronous "zipping…" spinner dies with the tab; the portal pattern is a job that finishes on its own.
The flow
The button calls your endpoint; the endpoint authorizes, signs that client's files, creates a job, and the portal shows the link when the job completes.
export async function POST(request: Request) {
const client = await authorizePortalUser(request); // your session check
// Sign only this client's folder — authorization happens here,
// before anything is signed.
const files = await listPrefixAsSignedUrls('portal-files', `clients/${client.id}/`);
const response = 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,
zip_filename: `${client.slug}-documents.zip`,
expires_in: 86400,
metadata: { client_id: client.id },
}),
});
const { job_id } = await response.json();
return Response.json({ job_id });
}For the portal UI, either poll GET /jobs/:id from the page until
status is completed and surface download_url, or let the
job.completed webhook write the link back to your database and notify
the client ("Your export is ready") — the webhook pattern also covers the
user who requested the export and left. Flow details, payloads, and
options are in
Create a ZIP from URLs with One API Call.
Decisions that matter for portals
- Sign per request, not per client-session. Short-lived signed URLs minted inside the authorized endpoint mean a leaked link exposes one export window, not standing access to the folder.
- The download link is safe to store and re-show. It expires on your
schedule (
expires_in) and needs no Eazip account to open — show it in the portal until it ages out, then offer the button again. - Set
fail_on_url_error: falseso one missing object produces a ZIP plus anerrorslist rather than a failed export — then fix the stray file instead of fielding a support ticket. - Very large lists can skip the browser entirely. If a client folder reaches thousands of objects, your endpoint is already server-side — the URL list never needs to leave it. That is also the boundary where a backend-created session fits if you later move the trigger into browser code.
When you don't need this
If portal folders are uniformly small — tens of files, tens of megabytes — the browser can do the zipping with no job and no account: Eazip.js fetches the same signed URLs and packages them client-side. The job path earns its keep when folders are heavy, when exports should survive navigation, or when you want the "your export is ready" email pattern.
FAQ
How do I make sure a client can only export their own files?
That check is your endpoint's: authorize the session, then list and sign only under that client's prefix. Eazip receives nothing but the URLs you chose to sign.
The user closed the tab — is the export lost?
No. The job runs server-side regardless of the tab. Store the link from the webhook and show it on next visit, or email it.
Can I let clients pick specific files instead of everything?
Yes — files is an arbitrary list. Post the selected IDs to your
endpoint, sign exactly those objects, and create the job the same way.
What does this cost per export?
Your storage's egress for one pass over the files (zero on R2), plus Eazip plan quota. Repeat downloads of the finished ZIP are served from zero-egress storage and don't touch your bucket again.
Deliver Event Photos to Clients as a ZIP
Turn a finished gallery into one expiring download link — hundreds of full-resolution photos, delivered without building ZIP infrastructure or paying bandwidth per download.
Build a User Data Export (GDPR Takeout) as a ZIP
Implement "download my data" as an async job — gather records and files, package them into one expiring ZIP, and email the link. The pattern GDPR Article 20 requests and account-deletion flows both need.