Move thumbnail / EXIF / dimensions / watermark / webhook work off the
upload request thread and into a background worker pool. Upload
requests now return 202 in seconds even on NFS-backed storage; the
worker(s) drain the pending queue independently and update each
photo's processing_status to 'complete' or 'failed' on its own.
Schema (migration 085_async_photo_processing.js):
- photos.processing_status enum default 'complete' (existing
rows are already done)
- photos.processing_error populated on 'failed'
- photos.processing_started_at timestamp for janitor recovery
- photos.upload_id groups all photos from one upload
request so the frontend can poll
status by group
- indexes on processing_status and upload_id for queue lookups
services/photoProcessor.js
- queueFilesForProcessing(files, options) — shared helper used by
the admin and gallery upload routes. Moves files to final storage
+ inserts pending rows; returns { uploadId, photos, errors }.
- processPhoto(photoId) — worker-mode: reads original from storage
via withLocalCopy (transparent local/S3), generates thumbnail and
EXIF/dimensions or video metadata, queues watermark, fires
photo.uploaded webhook, marks 'complete'. Throws => caller marks
'failed' with the error message.
- processUploadedPhotos kept untouched — chunkedUploadService still
uses the synchronous path.
services/backgroundProcessor.js (new)
- N independent worker loops per backend instance (default 2,
UPLOAD_PROCESSOR_CONCURRENCY env override).
- Multi-pod safe: postgres SELECT FOR UPDATE SKIP LOCKED, sqlite
UPDATE-with-status-guard. Pods race on rows, exactly one wins.
- Janitor every minute resets photos stuck in 'processing' for >10
minutes (worker died, pod restarted) back to 'pending'.
- UPLOAD_PROCESSOR_DISABLED=true opt-out for CI/test.
- Started from server.js after the other long-running workers.
routes/adminPhotos.js — POST /:eventId/upload
- Replaced batch-of-25 sync processing loop with per-file
move-to-storage + insert-pending. Response is now 202 with
upload_id, count, photo_ids in addition to the legacy
successCount / replacedCount fields the existing frontend reads.
- Per-request temp directory cleanup is now a single idempotent
handler on res.finish/res.close (was three inline blocks for
error paths only, leaking dirs on success — original bug from
contributor analysis).
- GET /uploads/:upload_id/status — JSON snapshot of pending /
processing / complete / failed counts plus per-photo state.
- GET /uploads/:upload_id/stream — SSE upgrade. Polls internally
every 1.5s, emits on snapshot change, ends when all photos
reach a terminal state.
- POST /photos/:photoId/retry — flips a 'failed' photo back to
'pending' so the worker picks it up again.
- GET /:eventId/thumbnail/:photoId now returns 503 with Retry-After
while the photo is still pending/processing, and 422 on 'failed'.
The admin grid renders placeholders accordingly.
routes/gallery.js — POST /:eventId/upload (guest)
- Refactored to use queueFilesForProcessing instead of the synchronous
processUploadedPhotos. Same 202 + upload_id shape.
- GET /:slug/photos now filters processing_status to 'complete' (or
NULL for pre-migration rows) so guests never see in-flight photos.
Side-effect timing change:
- photo.uploaded webhook now fires from the worker after the photo
is actually processed (thumbnail + dimensions populated) instead
of from inside the upload request. Same payload fields. Worth a
one-line note in the changelog.
📸 PicPeak - Open Source Photo Sharing for Events
PicPeak is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Designed specifically for photographers and event organizers, PicPeak makes it simple to share beautiful, time-limited photo galleries with clients while maintaining full control over your data and branding.
🎮 Live Demo
Try PicPeak without installing anything:
| Demo URL | demo.picpeak.app |
| Admin Panel | demo.picpeak.app/admin |
demo@picpeak.app |
|
| Password | Demo2026! |
The demo resets periodically. Uploaded content may be removed without notice.
🌟 Why Choose PicPeak?
Unlike expensive SaaS solutions, PicPeak gives you:
- 💰 No Monthly Fees - One-time setup, unlimited galleries
- 🔒 Complete Data Control - Your photos stay on your server
- 🎨 White-Label Ready - Full branding customization
- 📱 Mobile-First Design - Beautiful on all devices
- 🚀 Lightning Fast - Optimized performance and caching
- 🌍 Multi-Language - Built-in i18n support (EN, DE)
✨ Key Features
For Photographers
- 📁 Drag & Drop Upload - Simply drop photos into folders
- 🔗 External Media (Reference Mode) - Browse and import from a read‑only external folder library without copying originals
- ⏰ Auto-Expiring Galleries - Set expiration dates (default: 30 days)
- 🔐 Password Protection - Secure client galleries
- 📧 Automated Emails - Creation confirmations and expiration warnings
- 📊 Analytics Dashboard - Track views, downloads, and engagement
- 🎨 Custom Themes - Match your brand perfectly
- 🌐 Public Landing Page - Publish a curated marketing page when guests visit your root URL
For Clients
- 🖼️ Beautiful Galleries - Clean, modern interface
- 📱 Mobile Optimized - Swipe through photos on any device
- ⬇️ Bulk Downloads - Download all photos with one click
- 🔍 Smart Search - Find photos quickly
- 📤 Guest Uploads - Optional client photo uploads
- 🛡️ Download Protection - Advanced image protection with watermarking and right-click prevention
Technical Excellence
- 🐳 Docker Ready - Deploy in minutes
- 🔄 Auto-Processing - Automatic thumbnail generation
- 🗂️ Reference Library Support - Point PicPeak at
EXTERNAL_MEDIA_ROOTto reference existing originals, index quickly, and generate thumbnails on demand - 💾 Smart Storage - Automatic archiving of expired galleries
- 🛡️ Security First - JWT auth, rate limiting, CORS protection
- 📈 Scalable - From small studios to large agencies
🚀 Quick Start
Get PicPeak running in under 5 minutes:
# Clone the repository
git clone https://github.com/the-luap/picpeak.git
cd picpeak
# Copy environment template
cp .env.example .env
# Edit configuration (required: JWT_SECRET)
nano .env
# Start with Docker Compose
docker compose up -d
# Access at http://localhost:3000
Note on Docker file permissions (PUID/PGID)
- When using bind mounts (e.g.,
./storage,./data,./logs,./events), ensure the container user can write to these host folders. The backend runs as a non‑root user by default. - Set
PUIDandPGIDin your.envto match your host user’s UID/GID (runid -uandid -gon the host). Compose maps the container user to these values. - Example in
.env:PUID=1000PGID=1000
- Without this, creating events, uploads, thumbnails, or logs can fail with "Permission denied".
🔄 Release Channels
PicPeak offers two release channels for different needs:
Stable Channel (Recommended)
- Production-ready releases
- Thoroughly tested before release
- Docker tags:
stable,latest, or specific version likev2.3.0
Beta Channel
- Early access to new features
- May contain bugs or incomplete functionality
- Docker tags:
betaor specific version likev2.3.0-beta.1
Switching Channels
Set the PICPEAK_CHANNEL environment variable in your .env file:
# For stable releases (default)
PICPEAK_CHANNEL=stable
# For beta releases
PICPEAK_CHANNEL=beta
# For a specific version
PICPEAK_CHANNEL=v2.3.0
Then update your containers:
docker compose -f docker-compose.production.yml pull
docker compose -f docker-compose.production.yml up -d
Update Notifications
The admin dashboard automatically notifies you when updates are available for your channel. To disable update checks, set:
UPDATE_CHECK_ENABLED=false
📖 Documentation
Full documentation lives at docs.picpeak.app — deployment, admin settings reference, API docs, webhooks, archive lifecycle, branding, and everything else. Some quick links:
- 🚀 Deployment - Docker, environment variables, reverse proxy, SSL
- ⚙️ Admin Settings - Every tab in the Settings panel
- 🎯 Creating Events - Full event field reference
- 💾 Backup & Restore - Local, S3, rsync destinations
- 🔌 API Reference - REST endpoints, OpenAPI spec, webhooks
- 🪝 Webhooks - Event payloads, signing, filters, templates
Project meta:
- 🤝 Contributing - How to contribute
- 📜 License - MIT License
- 🔒 Security - Security policies
- 📋 Code of Conduct - Community guidelines
🌐 Public Landing Page
Spotlight your studio with a customizable marketing page at /:
- Head to Admin → CMS Pages to enable the public landing page toggle.
- Edit the provided HTML template (rich sections, hero, testimonials) and optional CSS overrides.
- The preview renders in a sandboxed iframe so you can iterate safely before publishing.
- PicPeak sanitizes stored HTML and CSS server-side—scripts, iframes, and unsafe attributes are stripped automatically.
- Use Reset to default anytime to restore the bundled template.
- The backend caches the rendered landing page for 60 seconds by default; override with
PUBLIC_SITE_CACHE_TTL_MSif you need a different TTL. - When the landing page is disabled PicPeak continues to serve the admin SPA/login exactly as before.
🎯 Use Cases
Perfect for:
- 💒 Wedding Photographers - Share ceremony photos securely
- 🎂 Event Photography - Birthday parties, corporate events
- 📸 Portrait Studios - Client galleries with download limits
- 🏢 Corporate Events - Internal photo sharing with branding
- 🎓 School Photography - Secure parent access with expiration
🏗️ Tech Stack
- Backend: Node.js, Express, SQLite/PostgreSQL
- Frontend: React, Tailwind CSS, Framer Motion
- Storage: Local filesystem (default) or S3-compatible object store (AWS S3, MinIO, R2, B2, Wasabi, Spaces) — see Storage Backends
- Email: SMTP with customizable templates
- Analytics: Privacy-focused with Umami integration
💾 Storage Backends
PicPeak supports two storage backends for photos, thumbnails, hero images, watermarks, and archive zips. Both are configured via environment variables; no code change is required to switch.
| Capability | STORAGE_BACKEND=local (default) |
STORAGE_BACKEND=s3 |
|---|---|---|
| Photo / thumbnail / hero storage | Local filesystem under STORAGE_PATH |
Bucket on any S3-compatible service |
| Admin UI upload | ✅ | ✅ |
| Filesystem auto-import (chokidar watcher) | ✅ | ❌ — disabled (use the upload API) |
| Watermarks, fingerprinting, fragmentation | ✅ | ✅ (materialized to a tmp file just-in-time) |
| Bulk download zips (cached + on-the-fly) | ✅ | ✅ |
| Backups | ✅ | ✅ |
External media reference mode (EXTERNAL_MEDIA_ROOT) |
✅ (always local) | ✅ (still local — not migrated) |
Switching to an S3-compatible backend
- Provision a bucket and credentials. The minimum IAM policy is documented in
.env.example. - Set
STORAGE_BACKEND=s3plusSTORAGE_S3_BUCKET,STORAGE_S3_REGION,STORAGE_S3_ACCESS_KEY,STORAGE_S3_SECRET_KEY. For non-AWS providers (MinIO, R2, B2, …) also setSTORAGE_S3_ENDPOINT. - If you have existing local content, copy it first:
node backend/scripts/migrate-storage.js --dry-runthennode backend/scripts/migrate-storage.js. The script is idempotent and writes a failures CSV. - Restart the backend. The startup check pings the bucket and refuses to boot on misconfig.
Note: presigned-URL serving (zero-bandwidth direct downloads from S3) is intentionally not in v1 — every request still streams through the backend so watermarks, devtools-detection, and access logging keep working.
🔔 Webhooks
PicPeak POSTs event/photo lifecycle notifications to URLs you configure under Settings → Webhooks. Each delivery is signed HMAC-SHA256 with a per-webhook secret in the X-PicPeak-Signature header so receivers can verify the request really came from your PicPeak instance.
Event types
| Event | Fires when |
|---|---|
event.created |
Gallery created (admin or API) |
event.published |
Draft becomes live (is_draft: true → false) — also fires when an event is created with is_draft=false |
event.archived |
Bulk-archive, manual archive, or auto-archive on expiry |
event.expired |
Expiration checker marks the gallery inactive (fires before event.archived in the cascade) |
photo.uploaded |
Admin upload, API upload, guest upload, or auto-import |
photo.deleted |
Single delete, bulk delete (NOT fired per-photo when an event is archived — receivers infer from event.archived to avoid flooding) |
Payload shape
{
"id": "delivery-uuid",
"type": "event.published",
"created_at": "2026-04-28T05:25:00.000Z",
"data": {
"event": { "id": 123, "slug": "wedding-smith", "share_url": "https://..." }
}
}
Also sent on every request:
X-PicPeak-Signature—HMAC-SHA256(secret, raw_body)as hexX-PicPeak-Event— the event type (handy for routing without parsing the body)X-PicPeak-Delivery— UUID for idempotency on the receiver sideUser-Agent: PicPeak-Webhooks/1.0
Verifying signatures
Node.js
const crypto = require('crypto');
function verify(secret, rawBody, signature) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(signature, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
Python
import hmac, hashlib
def verify(secret: str, raw_body: bytes, signature: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
curl + openssl (one-liner for a quick replay)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
[ "$SIG" = "$RECEIVED_SIG" ] && echo OK || echo MISMATCH
Retries + observability
2xx→ success, recorded with latency- Non-
2xxor network error → exponential backoff:1m → 5m → 30m → 2h → 12h, max 5 attempts - After max attempts: status
failed, surfaces in Settings → Webhooks → Deliveries with a "Replay" button - Up to 5 deliveries in flight at once; one slow consumer can't block others (configurable via
WEBHOOK_DELIVERY_CONCURRENCY) - Response body truncated to 1KB before storage so chatty receivers don't bloat the audit log
The deliveries page (/admin/webhooks/:id/deliveries) shows every attempt with timestamp, status, HTTP code, latency, payload sent, signature, and response. Click "Send test event" to fire a synthetic delivery for any event type.
SSRF protection
Webhook URLs are validated against the same private-IP blocklist used elsewhere in the app — loopback, private RFC1918 ranges, link-local, .local/.internal hostnames, cloud metadata endpoints. The check runs both at create time and per-delivery (DNS-rebinding mitigation).
For local development with a receiver on the same machine or docker network, set WEBHOOK_ALLOW_PRIVATE_URLS=true. Production deployments must leave this OFF.
💻 System Requirements
Minimum Requirements
- CPU: 2 CPU cores
- RAM: 2GB minimum
- Storage: 20GB minimum (plus photo storage needs)
- OS: Linux (Ubuntu 20.04+), macOS, or Windows with WSL2
- Node.js: v18.0.0 or higher
- Database: SQLite (included) or PostgreSQL 12+
Docker Requirements (Recommended)
- Docker: v20.10.0+
- Docker Compose: v2.0.0+
Video Support Requirements
When enabling video uploads, consider these additional resources:
| Resource | Recommendation | Notes |
|---|---|---|
| RAM | 4GB+ recommended | FFmpeg processing requires more memory |
| Storage | Plan for 10-100x more | Videos are significantly larger than images |
| CPU | Additional cores help | Video thumbnail extraction is CPU-intensive |
| Bandwidth | Higher throughput | Video streaming requires more bandwidth |
Technical Notes:
- FFmpeg is bundled via npm (
@ffmpeg-installer/ffmpeg) - no system installation required - Maximum upload size: 10GB per video file
- Chunked upload support for files >100MB (resumable uploads)
- Supported formats: MP4, WebM, MOV, AVI
- Video thumbnails are automatically generated from the first few seconds
For Nginx/Reverse Proxy: If using Nginx, increase the client max body size:
client_max_body_size 10G;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
🤝 Contributing
We love contributions! PicPeak is built by photographers, for photographers. Whether you're fixing bugs, adding features, or improving documentation, your help is welcome.
See our Contributing Guide for details.
📊 Comparison with Alternatives
| Feature | PicPeak | PicDrop | Scrapbook.de |
|---|---|---|---|
| Self-Hosted | ✅ | ❌ | ❌ |
| Custom Branding | ✅ Full | Limited | Limited |
| Monthly Cost | $0 | $29-199 | €19-99 |
| Storage Limit | Unlimited* | 50-500GB | 100-1000GB |
| Client Uploads | ✅ | ✅ | ✅ |
| API Access | ✅ | Paid | ❌ |
| Open Source | ✅ | ❌ | ❌ |
*Limited only by your server storage
🛡️ Security
PicPeak takes security seriously:
- 🔐 Password hashing with bcrypt
- 🎫 JWT-based authentication
- 🚦 Rate limiting on all endpoints
- 🛡️ CORS protection
- 📝 Activity logging
- 🔒 Secure file access
Found a security issue? Please open a security issue on GitHub
📸 Screenshots
🎛️ Admin Dashboard
Get a complete overview of your photo galleries, analytics, and system status.
📊 Analytics & Insights
Track gallery performance, view statistics, and monitor user engagement.
📁 Event Management
Organize and manage your photo galleries with intuitive event management tools.
✨ Key Interface Highlights
👆 Click to see more interface details
What makes PicPeak's interface special:
- 🎨 Clean Design: Modern, photographer-friendly interface
- 📱 Responsive: Perfect on desktop, tablet, and mobile
- ⚡ Fast Loading: Optimized for quick photo browsing
- 🔒 Secure Access: Password-protected galleries with expiration
- 📤 Easy Uploads: Drag & drop functionality for effortless photo management
- 🎯 Client-Focused: Intuitive gallery experience for your clients
🗺️ Roadmap
We're constantly improving PicPeak and welcome contributions from our community! If you have ideas for new features or want to help implement existing ones, please open an issue or submit a pull request. Your contributions help make PicPeak better for everyone.
🚧 Beta Features (Use at your own risk)
These features are currently in beta testing and may have limited functionality or stability:
| Feature | Description | Status |
|---|---|---|
| Simple Deployment Script | One-click deployment script for quick server setup with automated configuration and dependency installation | 🧪 Beta |
📋 Future Enhancements
| Feature | Description | Priority | Status |
|---|---|---|---|
| Backup & Restore | Comprehensive backup system with S3/MinIO support, automated scheduling, and safe restore functionality | High | ✅ Implemented |
| External Media Library (Reference Mode) | Use an external folder library as a read‑only source with import and on‑demand thumbnail generation | High | ✅ Implemented |
| Download Protection | Advanced image protection system with canvas rendering, invisible watermarking, right-click prevention, and DevTools detection to protect photos from unauthorized downloads | High | ✅ Implemented |
| Gallery Templates | Multiple gallery layouts (grid, masonry, carousel, timeline, hero, mosaic) with custom CSS styling support. Includes starter templates like Apple Liquid Glass for complete visual customization | Medium | ✅ Implemented |
| Face Recognition | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open |
| Gallery Feedback | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented |
| Video Support | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented |
| Multiple Administrators | Support for multiple admin accounts with role-based permissions and activity tracking | Low | ✅ Implemented |
| Filtering & Export Options | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented |
Status Legend: ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
☕ Support the Project
PicPeak is free, open source, and self-hostable forever. If it saves you time or replaces a paid subscription, consider buying me a coffee — it directly funds the time spent on new features, bug fixes, and keeping the demo + docs running.
Other ways to support without spending anything: ⭐ star the repo, share it with photographer friends, file good bug reports, or open a PR.
🙏 Acknowledgments
PicPeak is inspired by the best features of commercial platforms while remaining completely open source. Special thanks to all contributors who make this project possible.
🤖 AI-Assisted Development
This project was generated with the assistance of AI technology, but has been:
- ✅ Fully tested end-to-end by human developers
- 🔒 Security audited with comprehensive security checks
- 👨💻 Human-reviewed for code quality and best practices
- 🧪 Production-tested in real-world scenarios
We believe in transparent development practices and the responsible use of AI as a tool to accelerate development while maintaining high standards of quality and security.
📄 License
PicPeak is released under the MIT License. Use it freely for personal or commercial projects.
🚀 Ready to Get Started?
- ⭐ Star this repository to show your support
- 📖 Read the docs at docs.picpeak.app
- 🐛 Report issues or request features
- 🤝 Join our community and contribute!
Made with ❤️ by photographers, for photographers
Homepage •
Live Demo •
GitHub •
Documentation •
Support

