diff --git a/README.md b/README.md
index 91a29557..76aae2e6 100644
--- a/README.md
+++ b/README.md
@@ -1,91 +1,48 @@
-# 📸 PicPeak - Open Source Photo Sharing for Events
-
-> [!IMPORTANT]
-> **PicPeak has moved to its own GitHub organization.**
->
-> - **Docker images** are now published at `ghcr.io/picpeak/picpeak/{backend,frontend}`. Update your `docker-compose.yml`.
-> - ⚠️ The old path (`ghcr.io/the-luap/picpeak/...`) **still responds, but its tags are frozen** at 2026-05-27. `docker compose pull` succeeds and hands back the same build every time, so an out-of-date install looks like a broken download rather than a dead path. If PicPeak keeps reporting an update that never arrives, check your image path first.
-> - **Branches**: active development is now on `main` (was `beta`); the curated stable channel is now `stable` (was `main`). Existing PRs and clones auto-redirect via GitHub.
->
-> See **[`docs/migration-to-org.md`](docs/migration-to-org.md)** for the one-line `docker-compose.yml` edit and full details.
-

-
+
+ # 📸 PicPeak
+
+ **Open-source, self-hosted photo sharing for events.**
+
[](https://opensource.org/licenses/MIT)
[](https://www.docker.com/)
- [](https://nodejs.org/)
- [](https://reactjs.org/)
[](https://buymeacoffee.com/theluap)
- [Homepage](https://www.picpeak.app) · [Live Demo](https://demo.picpeak.app) · [Documentation](https://docs.picpeak.app) · [Support the project ☕](https://buymeacoffee.com/theluap)
+ [Homepage](https://www.picpeak.app) · [Live Demo](https://demo.picpeak.app) · [Documentation](https://docs.picpeak.app) · [Support ☕](https://buymeacoffee.com/theluap)
-**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.
+---
+
+**PicPeak** is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Built for photographers and event organizers, it makes it simple to share beautiful, time-limited photo galleries with clients while keeping full control over your data and branding.

+> [!IMPORTANT]
+> **PicPeak has moved to its own GitHub organization.** Docker images are now at `ghcr.io/picpeak/picpeak/{backend,frontend}` and active development is on `main`. The old `ghcr.io/the-luap/...` path still responds but its tags are **frozen** at 2026-05-27 — if updates never arrive, check your image path first. See **[`docs/migration-to-org.md`](docs/migration-to-org.md)** for the one-line `docker-compose.yml` edit.
+
+## Contents
+
+- [Live Demo](#-live-demo)
+- [Quick Start](#-quick-start)
+- [Why PicPeak?](#-why-picpeak)
+- [Features](#-features)
+- [Documentation](#-documentation)
+- [Comparison](#-comparison-with-alternatives)
+- [Tech Stack](#️-tech-stack)
+- [Contributing & Support](#-contributing)
+- [License](#-license)
+
## 🎮 Live Demo
-Try PicPeak without installing anything:
+Try PicPeak without installing anything — [demo.picpeak.app](https://demo.picpeak.app) · [admin panel](https://demo.picpeak.app/admin)
-| | |
+| Email | Password |
|---|---|
-| **Demo URL** | [demo.picpeak.app](https://demo.picpeak.app) |
-| **Admin Panel** | [demo.picpeak.app/admin](https://demo.picpeak.app/admin) |
-| **Email** | `demo@picpeak.app` |
-| **Password** | `Demo2026!` |
+| `demo@picpeak.app` | `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
-- 📽️ **Live Slideshow** - A separate fullscreen "Diashow" link per event for projectors at live events — auto-picks-up new uploads while it runs, with transitions, a logo watermark, and image-fit/colour options ([guide](docs/live-slideshow.md))
-- 🎨 **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_ROOT` to 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
-
-### For Studios — CRM & Accounting (Beta · off by default)
-- 📝 **Quotes → Contracts → Invoices** - One deal lineage; cancel-and-reissue (Storno) keeps issued invoices immutable
-- ⏱️ **Hours Logging & Calendar** - Per-customer time tracking; admin calendar of events, logged hours, and pending quotes/contracts
-- 🧾 **Inbound Supplier Invoices & Expenses** - Capture received invoices (upload/camera, rasterised server-side), categorise, and re-bill costs to clients
-- 📊 **Tax Report & Accountant Export** - Period-scoped income/cost report with VAT breakdown; PDF/CSV plus a Treuhänder/Banana (Swiss/LI) journal export, scopable to income-only or cost-only
-- 🌍 **VAT & Multi-currency** - Single VAT-code registry snapshotted onto each document; data-driven per-country rates
-- ⚠️ **Verify locally** - Feature-flagged off by default. Seeded contracts, QR/IBAN and tax defaults are **examples only** — review your own legal **and tax** regulations first (see disclaimers below)
-
## 🚀 Quick Start
Get PicPeak running in under 5 minutes:
@@ -97,8 +54,8 @@ cd picpeak
# Copy the environment template — the defaults work out of the box.
# Machine secrets (JWT, DB, Redis) are auto-generated on first run, and the
-# admin account is created in the browser (see below). Edit .env only to
-# customise (domain, SMTP, storage paths, …) — nothing is required.
+# admin account is created in the browser. Edit .env only to customise
+# (domain, SMTP, storage paths, …) — nothing is required.
cp .env.example .env
# Start with Docker Compose
@@ -107,293 +64,61 @@ docker compose up -d
# Access at http://localhost:3000
```
-### First run — create your admin account
+On first start, open **http://localhost:3000/admin** and follow the in-browser setup to create your admin account. Full details — the one-time setup token, Docker file permissions, and ARM64 notes — are in **[First-run setup](docs/_to-migrate/first-run-setup.md)**.
-On first start with no `ADMIN_PASSWORD` set, PicPeak has **no admin account yet** and greets you with an in-browser setup screen — no credentials in `.env`:
+> **Updating / release channels:** set `PICPEAK_CHANNEL` (`stable` default, or `beta`) in `.env`, then `docker compose pull && docker compose up -d`. See [RELEASING.md](RELEASING.md) for the promotion cadence.
-1. Open **http://localhost:3000/admin** — you'll be redirected to `/setup`.
-2. Read the **one-time setup token** from the 0600 file the backend writes it to
- (it is deliberately *not* printed to the logs — that would leave a live
- bootstrap credential in `docker logs`):
- ```bash
- docker compose exec backend cat /app/data/SETUP_TOKEN
- ```
- It is bind-mounted, so `sudo cat data/SETUP_TOKEN` on the host works too. Only
- if that file could not be written does the backend fall back to logging the
- token (`docker compose logs backend | grep -i "setup token"`).
-3. Paste the token, set your admin **email + password**, and you're in. The token is single-use, and the setup screen closes permanently once an admin exists.
+## 🌟 Why PicPeak?
-> Prefer the old behaviour? Set `ADMIN_PASSWORD` in `.env` and PicPeak auto-creates the admin on first boot instead (credentials written to `data/ADMIN_CREDENTIALS.txt`).
+Unlike expensive SaaS solutions, PicPeak gives you:
-Note on Docker file permissions
-- The backend container starts as root, chowns bind-mounted host directories (`./storage`, `./data`, `./logs`) to UID 1001 (`nodejs`), then drops privileges via `su-exec` before running the app. No host-side setup needed for fresh installs.
-- If you pin `user:` in a compose override (e.g. to map a specific host UID), the self-chown is skipped and you must pre-chown the host directories to that UID — see [docs.picpeak.app/deployment/docker#permissions](https://docs.picpeak.app/deployment/docker#permissions).
+- **💰 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
+- **🌍 Multi-Language** — built-in i18n (EN, DE)
-**ARM64 (aarch64) systems:** Pre-built images include native `linux/arm64`, no platform flags or emulation needed. If you're on an older image tag that's still amd64-only, see [docker-compose.amd64.override.yml](docker-compose.amd64.override.yml) for a transitional fallback.
+## ✨ Features
-## 🔄 Release Channels
+**For photographers** — drag & drop upload, auto-expiring & password-protected galleries, automated emails, an analytics dashboard, custom themes, a public landing page, and a [Live Slideshow](docs/live-slideshow.md) projector view that auto-picks-up new uploads during live events.
-PicPeak offers two release channels for different needs. Stable promotions are cut from a known-good beta point every 4–6 weeks — see [RELEASING.md](RELEASING.md) for the maintainer's promotion criteria and cadence policy.
+**For clients** — clean mobile-optimized galleries, one-click bulk downloads, smart search, optional guest uploads, and download protection (watermarking + right-click prevention).
-### Stable Channel (Recommended)
-- Production-ready releases
-- Thoroughly tested before release
-- Docker tags: `stable`, `latest`, or specific version like `v2.3.0`
+**Technical** — Docker-ready, automatic thumbnail generation, external media reference mode, smart archiving of expired galleries, S3-compatible [storage backends](docs/_to-migrate/storage-backends.md), [webhooks](docs/_to-migrate/webhooks.md), and security-first defaults (JWT, rate limiting, CORS).
-### Beta Channel
-- Early access to new features
-- May contain bugs or incomplete functionality
-- Docker tags: `beta` or specific version like `v2.3.0-beta.1`
+
+🧾 For studios — CRM & Accounting (Beta, off by default)
-### Switching Channels
+- 📝 **Quotes → Contracts → Invoices** — one deal lineage; cancel-and-reissue (Storno) keeps issued invoices immutable
+- ⏱️ **Hours Logging & Calendar** — per-customer time tracking; admin calendar of events, logged hours, and pending quotes/contracts
+- 🧾 **Inbound Supplier Invoices & Expenses** — capture received invoices (upload/camera, rasterised server-side), categorise, and re-bill costs to clients
+- 📊 **Tax Report & Accountant Export** — period-scoped income/cost report with VAT breakdown; PDF/CSV plus a Treuhänder/Banana (Swiss/LI) journal export
+- 🌍 **VAT & Multi-currency** — single VAT-code registry snapshotted onto each document
-Set the `PICPEAK_CHANNEL` environment variable in your `.env` file:
+
-```bash
-# 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:
-
-```bash
-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:
-
-```bash
-UPDATE_CHECK_ENABLED=false
-```
+> [!WARNING]
+> **CRM & Accounting — examples only, verify locally.** Feature-flagged off by default. Seeded contract blocks are written by the maintainer, **not a lawyer**; QR-bills/SEPA payloads and every tax, VAT and Treuhänder/Banana figure are computed from your input and defaults and are **jurisdiction-specific guidance only**. Have your lawyer review contracts, scan a test QR with your bank's app, and verify all numbers with your accountant / Treuhänder / tax authority before customer-facing use. Read **[docs/crm-disclaimers.md](docs/crm-disclaimers.md)** first.
## 📖 Documentation
-Full documentation lives at **[docs.picpeak.app](https://docs.picpeak.app)** — deployment, admin settings reference, API docs, webhooks, archive lifecycle, branding, and everything else. Some quick links:
+Full documentation lives at **[docs.picpeak.app](https://docs.picpeak.app)** — deployment, admin settings, API, branding, and more.
-- 🚀 [**Deployment**](https://docs.picpeak.app/deployment) - Docker, environment variables, reverse proxy, SSL
-- ⚙️ [**Admin Settings**](https://docs.picpeak.app/guides/admin-settings) - Every tab in the Settings panel
-- 🎯 [**Creating Events**](https://docs.picpeak.app/guides/creating-events) - Full event field reference
-- 📽️ [**Live Slideshow**](https://docs.picpeak.app/features/live-slideshow) - Fullscreen projector view that auto-updates during live events
-- 💾 [**Backup & Restore**](https://docs.picpeak.app/guides/backup-restore) - Backup configuration, restore wizard, full disaster recovery
-- 🔌 [**API Reference**](https://docs.picpeak.app/api) - REST endpoints, OpenAPI spec, webhooks
-- 🪝 [**Webhooks**](https://docs.picpeak.app/features/webhooks) - Event payloads, signing, filters, templates
-
-Project meta:
-
-- 🤝 [**Contributing**](CONTRIBUTING.md) - How to contribute
-- 📜 [**License**](LICENSE) - MIT License
-- 🔒 [**Security**](SECURITY.md) - Security policies
-- 📋 [**Code of Conduct**](CODE_OF_CONDUCT.md) - 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_MS` if 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
-- 📽️ **Live Events** - Put a [Live Slideshow](docs/live-slideshow.md) on the venue projector that updates as you shoot
-
-## 🏗️ 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](#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
-
-1. Provision a bucket and credentials. The minimum IAM policy is documented in `.env.example`.
-2. Set `STORAGE_BACKEND=s3` plus `STORAGE_S3_BUCKET`, `STORAGE_S3_REGION`, `STORAGE_S3_ACCESS_KEY`, `STORAGE_S3_SECRET_KEY`. For non-AWS providers (MinIO, R2, B2, …) also set `STORAGE_S3_ENDPOINT`.
-3. If you have existing local content, copy it first: `node backend/scripts/migrate-storage.js --dry-run` then `node backend/scripts/migrate-storage.js`. The script is idempotent and writes a failures CSV.
-4. 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 |
+| Topic | Link |
|---|---|
-| `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) |
+| 🚀 Deployment (Docker, env, reverse proxy, SSL) | [docs.picpeak.app/deployment](https://docs.picpeak.app/deployment) |
+| ⚙️ Admin settings reference | [docs.picpeak.app/guides/admin-settings](https://docs.picpeak.app/guides/admin-settings) |
+| 🎯 Creating events | [docs.picpeak.app/guides/creating-events](https://docs.picpeak.app/guides/creating-events) |
+| 📽️ Live Slideshow | [docs/live-slideshow.md](docs/live-slideshow.md) |
+| 💾 Backup & Restore | [docs/backup-restore.md](docs/backup-restore.md) |
+| 🔌 API reference | [docs.picpeak.app/api](https://docs.picpeak.app/api) |
+| 🪝 Webhooks | [docs/_to-migrate/webhooks.md](docs/_to-migrate/webhooks.md) |
+| 💾 Storage backends (local / S3) | [docs/_to-migrate/storage-backends.md](docs/_to-migrate/storage-backends.md) |
+| 💻 System requirements & tuning | [docs/_to-migrate/system-requirements.md](docs/_to-migrate/system-requirements.md) |
+| 🧾 CRM & Accounting | [docs.picpeak.app/features/crm](https://docs.picpeak.app/features/crm) · [disclaimers](docs/crm-disclaimers.md) |
+| 🗺️ Roadmap | [docs/_to-migrate/roadmap.md](docs/_to-migrate/roadmap.md) |
-### Payload shape
-
-```json
-{
- "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 hex
-- `X-PicPeak-Event` — the event type (handy for routing without parsing the body)
-- `X-PicPeak-Delivery` — UUID for idempotency on the receiver side
-- `User-Agent: PicPeak-Webhooks/1.0`
-
-### Verifying signatures
-
-**Node.js**
-```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**
-```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)
-```sh
-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-`2xx` or 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**: **4 GB minimum** for a normal photo-upload workload — sharp/libvips
- decodes the full uncompressed frame before resize, and the default two
- worker loops at sharp-concurrency 2 can push peak RSS past 1.5 GB on a
- batch of 20-MP+ photos. On a 2 GB VPS that's enough to OOM-kill the
- backend mid-batch (surfaces as 503s on thumbnails — see [Low-memory
- hosts](#low-memory-hosts) below for the recipe to run on 2 GB).
-- **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+
-
-### Low-memory hosts
-
-Running on 2 GB RAM (e.g. an entry-level VPS) is workable but requires
-tuning the upload-processor concurrency down. The backend auto-detects
-total RAM at startup via `os.totalmem()` — on a host that reports < 3 GB,
-it defaults `UPLOAD_PROCESSOR_CONCURRENCY` to **1** instead of 2 and logs
-a one-shot warning. You can pin the value explicitly in `.env`:
-
-```env
-# Single worker loop — slower batch processing, lower peak RSS
-UPLOAD_PROCESSOR_CONCURRENCY=1
-```
-
-The trade-off is throughput: a single worker processes one photo at a
-time, so a 100-photo batch takes ~2× as long but won't OOM. **Health-check
-note**: if the backend dies under memory pressure, the gallery serves
-`503 Service Unavailable` on thumbnails until Docker's
-`restart: unless-stopped` brings the container back. Persistent 503s
-during/after an upload batch on a low-memory host are almost always this.
-
-### 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:
-```nginx
-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](CONTRIBUTING.md) for details.
+**Project meta:** [Contributing](CONTRIBUTING.md) · [License](LICENSE) · [Security](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
## 📊 Comparison with Alternatives
@@ -410,168 +135,79 @@ See our [Contributing Guide](CONTRIBUTING.md) for details.
| Quotes / Contracts / Invoices | 🧪 Beta | ❌ | ❌ | ✅ |
| Incoming Invoices & Accounting | 🧪 Beta | ❌ | ❌ | ❌ |
-*You still bring your own server (own hardware or a VPS) and, if you want one, a domain.
-**Limited only by your server storage.
-***Pixieset's "unlimited" is photos only; video is capped by plan (roughly 0–10 h depending on tier).
-🧪 Beta = built but feature-flagged off by default (see [Beta Features](#-beta-features-use-at-your-own-risk)).
+*You bring your own server and, optionally, a domain. **Limited only by your server storage. ***Pixieset's "unlimited" is photos only; video is capped by plan. 🧪 Beta = built but feature-flagged off by default.
-## 🛡️ Security
+## 🏗️ Tech Stack
-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](https://github.com/PicPeak/picpeak/issues/new?labels=security) on GitHub
+- **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](docs/_to-migrate/storage-backends.md)
+- **Email**: SMTP with customizable templates
+- **Analytics**: Privacy-focused with Umami integration
+- **External media**: point PicPeak at `EXTERNAL_MEDIA_ROOT` to reference existing originals read-only, index quickly, and generate thumbnails on demand
## 📸 Screenshots
-### 🎛️ **Admin Dashboard**
-Get a complete overview of your photo galleries, analytics, and system status.
+
+Click to see the admin dashboard, analytics, and event management
+### 🎛️ Admin Dashboard
-### 📊 **Analytics & Insights**
-Track gallery performance, view statistics, and monitor user engagement.
-
+### 📊 Analytics & Insights
-### 📁 **Event Management**
-Organize and manage your photo galleries with intuitive event management tools.
-
+### 📁 Event Management
-### ✨ **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
+## 🤝 Contributing
-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.
+We love contributions! PicPeak is built by photographers, for photographers — whether you're fixing bugs, adding features, or improving docs. See the [Contributing Guide](CONTRIBUTING.md) to get started.
-### 🚧 Beta Features (Use at your own risk)
-
-These features are currently in beta testing and may have limited functionality or stability:
-
-| Feature | Description | Status |
-|---------|-------------|--------|
-| **CRM & Accounting Module** | Quotes, contracts, invoices (+ Storno), hours logging, calendar, and tax report — plus inbound supplier-invoice capture, internal expenses, and a Treuhänder/Banana (Swiss/LI) accountant-journal export. Feature-flagged off by default. Seeded contract blocks, payment terms, IBAN / QR-bill and tax defaults are **examples only** and need legal / financial / **tax** review before customer-facing use. See [docs.picpeak.app/features/crm](https://docs.picpeak.app/features/crm). | 🧪 Beta |
-| **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
+Found a security issue? Please open a [security issue](https://github.com/PicPeak/picpeak/issues/new?labels=security). See [SECURITY.md](SECURITY.md) for the policy.
## ☕ 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.
+PicPeak is free, open source, and self-hostable forever. If it saves you time or replaces a paid subscription, consider [buying me a coffee](https://buymeacoffee.com/theluap) — it directly funds new features, bug fixes, and keeping the demo + docs running. You can also ⭐ star the repo, share it, 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.
+PicPeak is inspired by the best features of commercial platforms while remaining completely open source. It's developed with AI assistance, but human-tested end-to-end, security-audited, and human-reviewed for quality.
### 👥 Contributors
A huge thank you to the people whose code, reports, and feedback have shaped PicPeak:
-- [**@the-luap**](https://github.com/the-luap) — creator and lead maintainer. Started the project and built PicPeak's foundation and the entire gallery experience (events, galleries, uploads, sharing, download protection, templates), plus backup & restore, analytics, system health, branding/theming, and WhatsApp notifications — and the architecture every later feature builds on.
-- [**@Luca-Timo**](https://github.com/Luca-Timo) — native Apple Silicon multi-arch images, external-URL toggle for legal CMS pages, the lazy-loaded folder tree picker, the admin-email picker on event creation, the data-driven self-hosted webfont system, the gallery header/banner decoupling, several typed-API refactors, and the CRM + accounting suite (quotes/contracts/invoices, hours logging, calendar, tax report, inbound supplier-invoice capture, expenses, and the Treuhänder/Banana export). Consistently raises the bar with thoughtful PRs.
-- [**@Rekoo-PS**](https://github.com/Rekoo-PS) — sharp-eyed bug reporter and product feedback. Filed the issues that drove the login-loop fix, the gallery-loading skeleton work, the redirection cleanup, the mobile-lightbox overhaul, the admin-events search-counter fix, the photo-count column, and the bulk-delete workflow. Also a [BuyMeACoffee](https://buymeacoffee.com/theluap) supporter — the kind of feedback loop that keeps the project useful for real deployments.
+**[@the-luap](https://github.com/the-luap)** — creator and lead maintainer
+- Gallery foundation (events, uploads, sharing, download protection, templates)
+- Backup & restore, analytics, branding/theming
+- The architecture every later feature builds on
+
+**[@Luca-Timo](https://github.com/Luca-Timo)**
+- Native Apple Silicon multi-arch images
+- CRM & accounting suite (quotes/contracts/invoices)
+- Hours logging & Treuhänder/Banana tax export
+- Gallery header/banner decoupling
+
+**[@Rekoo-PS](https://github.com/Rekoo-PS)** — bug reports & product feedback
+- Login-loop fix, mobile-lightbox overhaul, bulk-delete workflow
+- Also a [BuyMeACoffee](https://buymeacoffee.com/theluap) supporter
If you've contributed and aren't listed here, please open a PR — this list is meant to grow.
-### 🤖 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.
-
-## ⚠️ CRM & Accounting disclaimers — examples only, verify locally
-
-The CRM & accounting modules (contracts, invoices, QR-bills, the tax
-report and the accountant exports) ship seeded content and computed
-figures that are intended as a **starting point only**:
-
-- **Contract blocks** (image rights, NDA, model release, cancellation,
- jurisdiction, …) are written by the maintainer, **not by a lawyer**.
- Every operator must have their lawyer review and adapt them before
- sending any contract to a customer.
-- **QR-bills and SEPA EPC payloads** are rendered from the data you
- typed. Picpeak is open source — please scan a test invoice with your
- bank's app to check the QR actually works. We are not responsible for
- any mistakes that come from sending an invoice with bad data on it.
-- **Tax, VAT & accounting figures** (the tax report, VAT-payable, the
- per-rate breakdown, the Treuhänder / Banana export, etc.) are computed
- from the data you enter and the defaults you configure. They are
- **guidance only and jurisdiction-specific** — tax rules, VAT rates,
- deduction schemes (e.g. the Liechtenstein 20 % Gewinnungskosten flat
- rate) and filing duties differ by country and change over time. **Every
- operator must check their own tax / VAT regulations and verify the
- numbers with their accountant / Treuhänder / tax authority before
- relying on any figure or export.** Picpeak makes no warranty that the
- output is correct for your jurisdiction or situation.
-
-Read [`docs/crm-disclaimers.md`](docs/crm-disclaimers.md) before
-enabling the Contracts, Invoices or Accounting features.
-
## 📄 License
PicPeak is released under the [MIT License](LICENSE). Use it freely for personal or commercial projects.
-## 🚀 Ready to Get Started?
-
-1. ⭐ **Star this repository** to show your support
-2. 📖 Read the [docs at docs.picpeak.app](https://docs.picpeak.app)
-3. 🐛 Report issues or request features
-4. 🤝 Join our community and contribute!
-
---
Made with ❤️ by photographers, for photographers
- Homepage •
- Live Demo •
- GitHub •
- Documentation •
+ Homepage ·
+ Live Demo ·
+ Documentation ·
Support
diff --git a/docs/_to-migrate/README.md b/docs/_to-migrate/README.md
new file mode 100644
index 00000000..d63301d4
--- /dev/null
+++ b/docs/_to-migrate/README.md
@@ -0,0 +1,22 @@
+# `_to-migrate` — staging area, **not canonical**
+
+> [!WARNING]
+> These pages were cut out of the root `README.md` to slim it down. They are
+> **being ported to the PicPeak docs site** (its own repo → docs.picpeak.app).
+>
+> - **Do not edit** these files expecting the change to be authoritative — edit
+> the docs-site copy once it exists.
+> - **Do not add new links** to this folder from code or other docs.
+> - This whole folder is **deleted** once the docs-site pages are live (see the
+> 3-phase plan in PicPeak/picpeak#1000).
+
+The root README links here only as a temporary bridge so no link 404s while the
+docs-site pages are being written.
+
+| File | Ported to docs.picpeak.app path |
+|---|---|
+| `webhooks.md` | `/features/webhooks` |
+| `storage-backends.md` | `/deployment` (storage section) |
+| `first-run-setup.md` | `/deployment` (first run / permissions) |
+| `system-requirements.md` | `/deployment` (system requirements) |
+| `roadmap.md` | project roadmap / GitHub Projects (TBD) |
diff --git a/docs/_to-migrate/first-run-setup.md b/docs/_to-migrate/first-run-setup.md
new file mode 100644
index 00000000..14fd38ac
--- /dev/null
+++ b/docs/_to-migrate/first-run-setup.md
@@ -0,0 +1,26 @@
+# First run — create your admin account
+
+On first start with no `ADMIN_PASSWORD` set, PicPeak has **no admin account yet** and greets you with an in-browser setup screen — no credentials in `.env`:
+
+1. Open **http://localhost:3000/admin** — you'll be redirected to `/setup`.
+2. Read the **one-time setup token** from the 0600 file the backend writes it to
+ (it is deliberately *not* printed to the logs — that would leave a live
+ bootstrap credential in `docker logs`):
+ ```bash
+ docker compose exec backend cat /app/data/SETUP_TOKEN
+ ```
+ It is bind-mounted, so `sudo cat data/SETUP_TOKEN` on the host works too. Only
+ if that file could not be written does the backend fall back to logging the
+ token (`docker compose logs backend | grep -i "setup token"`).
+3. Paste the token, set your admin **email + password**, and you're in. The token is single-use, and the setup screen closes permanently once an admin exists.
+
+> Prefer the old behaviour? Set `ADMIN_PASSWORD` in `.env` and PicPeak auto-creates the admin on first boot instead (credentials written to `data/ADMIN_CREDENTIALS.txt`).
+
+## Docker file permissions
+
+- The backend container starts as root, chowns bind-mounted host directories (`./storage`, `./data`, `./logs`) to UID 1001 (`nodejs`), then drops privileges via `su-exec` before running the app. No host-side setup needed for fresh installs.
+- If you pin `user:` in a compose override (e.g. to map a specific host UID), the self-chown is skipped and you must pre-chown the host directories to that UID — see [docs.picpeak.app/deployment/docker#permissions](https://docs.picpeak.app/deployment/docker#permissions).
+
+## ARM64 (aarch64) systems
+
+Pre-built images include native `linux/arm64`, no platform flags or emulation needed. If you're on an older image tag that's still amd64-only, see [docker-compose.amd64.override.yml](../../docker-compose.amd64.override.yml) for a transitional fallback.
diff --git a/docs/_to-migrate/roadmap.md b/docs/_to-migrate/roadmap.md
new file mode 100644
index 00000000..6002839b
--- /dev/null
+++ b/docs/_to-migrate/roadmap.md
@@ -0,0 +1,28 @@
+# 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 |
+|---------|-------------|--------|
+| **CRM & Accounting Module** | Quotes, contracts, invoices (+ Storno), hours logging, calendar, and tax report — plus inbound supplier-invoice capture, internal expenses, and a Treuhänder/Banana (Swiss/LI) accountant-journal export. Feature-flagged off by default. Seeded contract blocks, payment terms, IBAN / QR-bill and tax defaults are **examples only** and need legal / financial / **tax** review before customer-facing use. See [docs.picpeak.app/features/crm](https://docs.picpeak.app/features/crm). | 🧪 Beta |
+| **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
diff --git a/docs/_to-migrate/storage-backends.md b/docs/_to-migrate/storage-backends.md
new file mode 100644
index 00000000..d6afc191
--- /dev/null
+++ b/docs/_to-migrate/storage-backends.md
@@ -0,0 +1,22 @@
+# 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
+
+1. Provision a bucket and credentials. The minimum IAM policy is documented in `.env.example`.
+2. Set `STORAGE_BACKEND=s3` plus `STORAGE_S3_BUCKET`, `STORAGE_S3_REGION`, `STORAGE_S3_ACCESS_KEY`, `STORAGE_S3_SECRET_KEY`. For non-AWS providers (MinIO, R2, B2, …) also set `STORAGE_S3_ENDPOINT`.
+3. If you have existing local content, copy it first: `node backend/scripts/migrate-storage.js --dry-run` then `node backend/scripts/migrate-storage.js`. The script is idempotent and writes a failures CSV.
+4. 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.
diff --git a/docs/_to-migrate/system-requirements.md b/docs/_to-migrate/system-requirements.md
new file mode 100644
index 00000000..33984cfa
--- /dev/null
+++ b/docs/_to-migrate/system-requirements.md
@@ -0,0 +1,63 @@
+# System Requirements
+
+## Minimum Requirements
+- **CPU**: 2 CPU cores
+- **RAM**: **4 GB minimum** for a normal photo-upload workload — sharp/libvips
+ decodes the full uncompressed frame before resize, and the default two
+ worker loops at sharp-concurrency 2 can push peak RSS past 1.5 GB on a
+ batch of 20-MP+ photos. On a 2 GB VPS that's enough to OOM-kill the
+ backend mid-batch (surfaces as 503s on thumbnails — see [Low-memory
+ hosts](#low-memory-hosts) below for the recipe to run on 2 GB).
+- **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+
+
+## Low-memory hosts
+
+Running on 2 GB RAM (e.g. an entry-level VPS) is workable but requires
+tuning the upload-processor concurrency down. The backend auto-detects
+total RAM at startup via `os.totalmem()` — on a host that reports < 3 GB,
+it defaults `UPLOAD_PROCESSOR_CONCURRENCY` to **1** instead of 2 and logs
+a one-shot warning. You can pin the value explicitly in `.env`:
+
+```env
+# Single worker loop — slower batch processing, lower peak RSS
+UPLOAD_PROCESSOR_CONCURRENCY=1
+```
+
+The trade-off is throughput: a single worker processes one photo at a
+time, so a 100-photo batch takes ~2× as long but won't OOM. **Health-check
+note**: if the backend dies under memory pressure, the gallery serves
+`503 Service Unavailable` on thumbnails until Docker's
+`restart: unless-stopped` brings the container back. Persistent 503s
+during/after an upload batch on a low-memory host are almost always this.
+
+## 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:
+```nginx
+client_max_body_size 10G;
+proxy_read_timeout 3600;
+proxy_send_timeout 3600;
+```
diff --git a/docs/_to-migrate/webhooks.md b/docs/_to-migrate/webhooks.md
new file mode 100644
index 00000000..bb497018
--- /dev/null
+++ b/docs/_to-migrate/webhooks.md
@@ -0,0 +1,77 @@
+# 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
+
+```json
+{
+ "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 hex
+- `X-PicPeak-Event` — the event type (handy for routing without parsing the body)
+- `X-PicPeak-Delivery` — UUID for idempotency on the receiver side
+- `User-Agent: PicPeak-Webhooks/1.0`
+
+## Verifying signatures
+
+**Node.js**
+```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**
+```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)
+```sh
+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-`2xx` or 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.