CLIENT DELIVERY PORTAL
Password-protected photo delivery for photography clients. Photos are stored in Cloudflare R2 and served via short-lived presigned URLs. Galleries expire automatically; a nightly function cleans up the files.
Overview
Built as a branded alternative to WeTransfer or Google Drive for delivering finished photo galleries. A single CLI command uploads an edited folder to Cloudflare R2, generates a password-protected gallery URL with a configurable expiry, and prints the shareable link ready to send to the client.
Everything runs on dvedee.com via Netlify Functions — no separate server, no third-party gallery platform, no per-download fees.
Uploading a Gallery
One command uploads the folder, generates the gallery, and prints the shareable link:
node scripts/upload-gallery.js /path/to/photos/folder
The script prompts for client name, password, expiry duration, and which folder to show first. When it finishes:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Gallery ready!
Client: Sarah & John
URL: https://dvedee.com/client-gallery?id=abc123...
Password: bluemoon42
Expires: Wed May 22 2026
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
What Clients Experience
-
Unlock
Client opens the link and enters their password. The gallery unlocks and their session stays active for 4 hours without needing to re-enter it.
-
Browse
Photos are organised into tabs (Social, High Res, etc.) based on the folder structure. Clients can browse full-screen and switch between tabs.
-
Download
Individual photos, per-tab zip files, or a full-gallery zip are all available. Downloads are served via short-lived presigned R2 URLs — never routed through Netlify.
-
Notification
A Resend email is sent when a client downloads, with a timezone-localised timestamp. The gallery then expires automatically on the set date and files are cleaned up from R2 overnight.
Technical Details
- Storage: photos, per-folder zips, a master zip, and a
meta.jsonmanifest are stored in R2 under a random gallery ID - Auth: password is checked against a SHA-256 hash in
meta.json; a short-lived HMAC session token (4 hours) is returned on success - Rate limiting: 5 failed password attempts per IP per hour before lockout
- Downloads: presigned R2 URLs are generated on demand and valid for 1 hour — actual files never pass through Netlify
- Auto-cleanup: a Netlify scheduled function runs at 3am UTC daily and deletes R2 files for any expired gallery
- Analytics: a private dashboard shows each gallery, when it was opened, and when downloads happened