A bundle of v0.16 platform work landing on top of the SvelteKit viewer.
Brings in 7 commits across docs, backend infra + features, frontend
plumbing, the live diashow, all UI surfaces (with dark mode), and a
viewer rebuild.
Headline features:
- Live diashow with two-queue policy (live drains first, shuffle as
fallback) and pluggable transitions. Design: docs/CONCEPT_DIASHOW.md.
- Dark mode: 'system' / 'light' / 'dark' preference, picked in the
onboarding step + the Account page, applied via Tailwind v4
class-based dark variant with FOUC guard.
- Host/Admin PIN reset with one-time PIN modal; admin may also reset
host PINs. Hosts may demote other hosts.
- Per-user dynamic storage quota enforced on upload + live widget in
My Account and on the upload screen. Toggleable per-area.
- All rate-limits + quotas individually toggleable from the admin
config UI (rendered as switches + a privacy_note textarea).
- Mobile-first gestures: long-press → context sheet, double-tap to
like with heart-burst. Buttons stay as desktop equivalents.
- Data mode (Saver vs Original) per device, applied across feed,
lightbox, and diashow.
- Per-event Datenschutzhinweis admin-editable, live-refreshed on all
clients via SSE event-updated.
- /api/v1/upload/{id}/original endpoint, /me/context + /me/quota.
Hardening (latent issues from the long-term review):
- Startup recovery for stuck compression / export jobs after a crash.
- Hourly cleanup of expired sessions + cold rate-limiter HashMap keys.
- ffmpeg 120s timeout with kill_on_drop (no more permit leaks).
- Per-user IndexedDB upload queue (no more cross-user leak on shared
devices); IDB schema bumped to v2.
- SSE reconnect uses exponential backoff (no more retry storm).
- PIN lockout no longer escalates — attempts reset when the cooldown
expires.
- soft_delete is now transactional and decrements total_upload_bytes
so quotas don't drift.
- pin-reset SSE handler filters by user_id so a host resetting Anna's
PIN doesn't clear Bob's cached PIN.
- Privacy note shown preformatted; admin-editable, ≤16 KiB cap.
Docs:
- New: FEATURES.md (role matrix), USER_JOURNEYS.md, IDEAS.md,
CONCEPT_DIASHOW.md, backend/migrations/README.md,
frontend/src/lib/README.md.
- Refresh: PROJECT.md, README.md, TEST_GUIDE.md, the two existing
CONCEPT_*.md banners.
EventSnap
A private, QR-code-accessed photo & video sharing platform for weddings, birthdays, and personal events — built for guests, run by you.
What is EventSnap?
At private events, photos and videos are scattered across dozens of guests' phones and never truly shared. Existing solutions (WhatsApp groups, Google Photos) require accounts, expose personal data, and lack event-specific social features.
EventSnap gives every guest instant, frictionless access to a shared, living gallery — no app store, no email, no password.
A guest scans the QR code on their way in, types their name, and is immediately part of a shared moment. They upload, react, and comment throughout the day. After the event, the host releases the gallery — every guest walks away with a beautiful offline HTML keepsake and the full archive.
Project type: Mobile-first PWA — runs in any browser, no installation required.
Scale: Personal / private use — one event at a time, ~100 guests, ~1,000 files.
Features
MVP
| Area | Feature |
|---|---|
| Onboarding | QR code join flow, name-only registration, persistent JWT + recovery PIN, 30-day sessions |
| Uploads | Photo & video from library or live camera, client-side IndexedDB queue, per-file progress & retry, captions + #hashtags |
| Processing | Lossless server-side compression, feed preview generation, ffmpeg video thumbnails |
| Feed | Chronological grid, real-time SSE updates, hashtag filtering, likes & comments |
| Host Dashboard | Ban/unban guests, delete content, promote to host, lock event, release gallery for export |
| Admin Dashboard | All host permissions + configure limits, rates, quota tolerance, disk usage widget |
| Export | On-demand ZIP (full-quality originals) + self-contained offline Memories.html viewer |
Planned (v1.x)
- Individual file download button
- Low-disk alert (< 10 GB free)
- Event banner / cover image
- Chunked resumable upload for large videos
- Host-curated story highlights
- Slideshow / presentation mode
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | SvelteKit + TypeScript |
| Styling | Tailwind CSS v4 |
| Backend | Rust + Axum |
| Async | Tokio |
| Database | PostgreSQL 16 via SQLx (compile-time query checking) |
| Auth | Custom JWT (jsonwebtoken) + bcrypt PINs |
| Image processing | image crate + oxipng (lossless compression) |
| Video processing | ffmpeg via tokio::process::Command |
| File storage | Local disk (/media/) |
| Real-time | Axum SSE + tokio::sync::broadcast |
| Export | async-zip (streaming ZIP) + minijinja (HTML bundle) |
| Rate limiting | tower-governor (token-bucket, DB-configurable) |
| Reverse proxy | Caddy 2 (automatic HTTPS via Let's Encrypt) |
| Containers | Docker + Docker Compose |
| Infrastructure | Hetzner CX33 (4 vCPU, 8 GB RAM, 80 GB SSD) |
Repository Structure
eventsnap/
├── backend/ # Rust + Axum API server
│ ├── src/
│ ├── Cargo.toml
│ └── Dockerfile
├── frontend/ # SvelteKit PWA
│ ├── src/
│ ├── svelte.config.js
│ └── Dockerfile
├── docker-compose.yml
├── Caddyfile
└── .env.example
Getting Started
Prerequisites
- Docker (includes Compose plugin)
- A domain name with an A record pointing to your server
Deploy on a fresh VPS
# 1. Clone the repository
git clone https://git.mc02.dev/fabi/EventSnap.git
cd EventSnap
# 2. Configure environment
cp .env.example .env
nano .env # set DOMAIN, JWT_SECRET, ADMIN_PASSWORD_HASH, EVENT_NAME, etc.
# 3. Start the stack
docker compose up -d
Caddy automatically obtains a Let's Encrypt certificate on first start. The app is live at https://DOMAIN within ~30 seconds.
Generate required secrets
# JWT secret (64 random bytes)
openssl rand -hex 64
# Admin password hash (bcrypt, cost 12)
htpasswd -bnBC 12 "" yourpassword | tr -d ':\n'
Environment Variables
See .env.example for the full list with descriptions and defaults. Key variables:
| Variable | Description |
|---|---|
DOMAIN |
Public domain for TLS (e.g. my-wedding.example.com) |
JWT_SECRET |
64-byte random hex string for signing JWTs |
ADMIN_PASSWORD_HASH |
bcrypt hash of the admin dashboard password |
EVENT_NAME |
Display name shown to guests |
EVENT_SLUG |
URL-safe event identifier |
DATABASE_URL |
PostgreSQL connection string |
Docker Compose Stack
┌─────────────────────────────────────┐
│ Caddy :80 / :443 (TLS termination) │
└────────────┬────────────────────────┘
│
┌────────┴────────┐
│ │
┌───▼────┐ ┌─────▼──────┐
│ app │ │ frontend │
│ :3000 │ │ :3001 │
│ (Rust) │ │(SvelteKit) │
└───┬────┘ └────────────┘
│
┌───▼────┐
│ db │
│ :5432 │
│(Postgres)│
└────────┘
/api/*and/media/*→ Rust backend- Everything else → SvelteKit frontend (
adapter-node) - Named volumes:
postgres_data,media_data,caddy_data
Backup
# Database snapshot
pg_dump $DATABASE_URL | gzip > /media/backups/db_$(date +%Y-%m-%d).sql.gz
# Weekly offsite sync (Hetzner Storage Box or similar)
rsync -az /opt/eventsnap/media/ user@storagebox.example.com:backup/eventsnap/
The /media volume holds originals, previews, thumbnails, exports, and DB backups — a single path to back up.
Development Roadmap
Done:
- Project blueprint & architecture
- Monorepo scaffold (
backend/,frontend/, Docker Compose) - DB schema + SQLx migrations (8 migrations through compression status + case-insensitive unique names)
- Auth flow (join, JWT, 4-digit PIN with bcrypt + 3-attempt/15-min lockout, admin login)
- Upload pipeline (multipart → compression worker via
tokio::sync::Semaphore→ SSE broadcast) - Client upload queue (IndexedDB, progress, retry, rate-limit auto-resume)
- Gallery feed (list + grid toggle, SSE live updates, hashtag chips, in-memory search + autocomplete)
- Camera capture (
getUserMediawith front/back toggle, photo +MediaRecordervideo) - Host Dashboard (event lock, gallery release, ban modal with hide-uploads choice, promote/demote, user search)
- Admin Dashboard with inner tabs (Stats, Config, Export, Nutzer)
- Export engine: streaming ZIP + SvelteKit-static HTML viewer (see docs/CONCEPT_HTML_VIEWER.md)
- Custom rate limiter (per-endpoint, hot-reloadable from
configtable) - Mobile-first redesign (bottom nav + FAB, see docs/CONCEPT_MOBILE_UI.md)
Open:
- Dynamic per-user storage quota enforcement (formula in PROJECT.md §12; only tracking exists today)
- Own-upload deletion UI in the lightbox (backend route exists)
- SSE delta-fetch on foreground reconnect (scaffolded in sse.ts, not wired)
- Live diashow / slideshow mode — see docs/CONCEPT_DIASHOW.md
- Individual file download button per post
- Low-disk alert (< 10 GB free)
- Event banner / cover image
- Chunked resumable upload for files > 100 MB
- Shared Tailwind config between main app and export-viewer
- End-to-end test event (10+ real devices on cellular)
See docs/FEATURES.md for the up-to-date capability matrix by role. Speculative / v2+ ideas live in docs/IDEAS.md.
License
Private project — all rights reserved.