When the timeline navigates to a nested folder (RightSidebar's open-folder icon, URL hydration, back/forward), the LeftSidebar already highlighted the matching row via filters.folderPath — but if the parent folder was collapsed in the persisted openSet, the highlighted row wasn't visible at all. Each FolderTree instance now runs an effect that adds every ancestor of the active path to its openSet on filter change. The root instance expands the top-level ancestor first, which mounts the next-depth FolderTree instance — and the same effect runs there, cascading down to the leaf. Persisted to localStorage so the expansion sticks across reloads. Skipped in `readonly` mode (heap-convert picker has its own selectedPath and shouldn't drive the sidebar state). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mule-image
Self-hosted photo management built on top of PhotoPrism. A SvelteKit
frontend (web/) plus a small Go service (sidecar/)
fill in the keyboard-driven UI and the file/folder/mark endpoints
PhotoPrism's REST API does not expose. PhotoPrism itself handles
indexing, originals, thumbnails, and the database; we never re-implement
those.
Architecture
┌──────────────────┐ /api/v1/* ┌──────────────┐
│ SvelteKit web/ │ ───────────────▶ │ photoprism │ ──▶ mariadb
│ (Vite : 5173) │ /api/sidecar/* │ :2342 │
│ │ ─────────┐ └──────────────┘
└──────────────────┘ ▼
┌──────────────┐
│ sidecar │ ──▶ mariadb (mule_sidecar.*)
│ :8000 │ ──▶ originals FS (rename / folders / dups)
└──────────────┘
Three compose services — mariadb, photoprism, sidecar — plus the
SvelteKit web/ app served separately. PhotoPrism's port 2342 is
bound to 127.0.0.1 only; it isn't a user-facing surface. The
SvelteKit app is.
What the sidecar adds on top of PhotoPrism (full list in
sidecar/README.md):
- Per-photo marks (rating + color) persisted to
mule_sidecar.marks - File rename + folder create/rename/delete with PhotoPrism reindex
- Heap (album) → folder conversion
- Perceptual-hash duplicate scan + archive
Quick start
cp .env.example .env
# edit .env: set PHOTO_DIRS to the host path holding your library
# rotate PP_ADMIN_PASSWORD, PP_DB_PASSWORD, PP_DB_ROOT_PASSWORD
# before any non-local deployment.
podman-compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.podman.yml \
up -d
Then serve the frontend. For local use the simplest path is the Vite dev server:
cd web
npm install
npm run dev
# open http://localhost:5173
For a static deployment, npm run build produces a bundle under
web/build/ that any static file host (nginx, Caddy, GitHub Pages-style)
can serve. Reverse-proxy /api/v1/* to http://127.0.0.1:2342 and
/api/sidecar/* to http://127.0.0.1:8000.
PhotoPrism's own UI is still reachable from the host at
http://127.0.0.1:2342 if you need admin features (user management,
settings) — set up an SSH tunnel from your laptop if the server is
remote.
Configuration
All knobs live in .env.example. The required ones:
| Variable | Notes |
|---|---|
PHOTO_DIRS |
Host path mounted at /photoprism/originals. The library. |
PP_ADMIN_PASSWORD |
First-boot admin password. Rotate. |
PP_DB_PASSWORD |
MariaDB password for the photoprism user. Rotate. |
PP_DB_ROOT_PASSWORD |
MariaDB root password. Rotate. |
PP_UID / PP_GID |
Host UID/GID that owns PHOTO_DIRS. PhotoPrism + sidecar drop to this user inside. |
PP_PORT |
Loopback host port for PhotoPrism (default 2342). |
PP_ORIGINALS_MODE |
rw (default) or ro — see Read-only libraries. |
SIDECAR_PORT |
Loopback host port for the sidecar (default 8000). |
Sidecar-specific env (DB DSN, USER_BASEPATHS, etc.) is documented in
sidecar/README.md.
Read-only libraries
The default originals mount is :rw because file operations (rename,
folder mutations, duplicate archive, heap convert) need to mutate the
filesystem. To run against a read-only archive, set
PP_ORIGINALS_MODE=ro in .env. Browsing, marks, ratings, and color
labels still work; the following sidecar endpoints return an OS error:
POST /api/sidecar/files/:uid/renamePOST /api/sidecar/folders/:rel/rename/DELETE /:relPOST /api/sidecar/albums/:uid/convertPOST /api/sidecar/duplicates/archive
PhotoPrism's PHOTOPRISM_READONLY is controlled separately by
PP_READONLY and gates its own backwrite / import paths.
Dev iteration loop
For fast iteration on the sidecar without rebuilding its image on every
change, run it as a host process — bring up just mariadb and
photoprism from compose, then build and run the Go binary locally.
Full instructions in sidecar/README.md.
Layout
.
├── docker-compose.yml base stack: mariadb + photoprism + sidecar
├── docker-compose.podman.yml rootless-podman overlay (keep-id mapping)
├── .env.example required env vars (copy to .env)
├── mariadb/init/ first-boot SQL: creates mule_sidecar DB + user
├── pp/ PhotoPrism bind-mounted state (storage, import)
├── sidecar/ Go service — see sidecar/README.md
└── web/ SvelteKit frontend