The Problem
Self-hosting a photo gallery usually means either a heavy platform like Nextcloud or PhotoPrism, or a static file browser that ignores metadata and presentation. Foldergram targets the middle: a lightweight, local-first gallery that maps real folders to Instagram-style profiles and feeds, without sending media to a third party.
What This Does
Foldergram is a self-hosted web app with a Vue 3 client (client/src/) and a Node/TypeScript server (server/src/). It indexes media from a configured GALLERY_ROOT, stores metadata in SQLite (server/src/db/repositories.ts), and generates thumbnails/previews via server/src/services/derivative-service.ts. The UI mirrors Instagram: home feed, reels, explore, places, and folder "profiles."
The server exposes REST routes (server/src/routes/api.ts) and a lazy-derivative endpoint (server/src/routes/lazy-derivatives.ts). Client state lives in Pinia stores (client/src/stores/), and API calls are centralized in client/src/api/gallery.ts. The app is PWA-capable with a service worker (client/public/sw.js).
How It Is Wired
Execution starts at bootstrap in client/src/main.ts, which reaches 88 functions. The client calls fetchFeed, fetchReels, etc., through client/src/api/gallery.ts. On the server, start in server/src/db/repositories.ts (line 2412) reaches 48 functions and is the entry point for the database layer.
The call graph shows heavy hub modules: client/src/types/api.ts is imported by 46 modules, and server/src/utils/image-utils.ts by 28. Changing either ripples widely. The server's scanner-service.ts (77 functions) and gallery-service.ts (113 functions) are oversized and carry most business logic.
Database writes happen through prepare (called from 126 places), filesystem access via fsPromises.mkdir, and the shortest traced path from bootstrap to a DB write is bootstrap -> ensureDefaultCollection — two hops. The wiring is straightforward: HTTP request → route handler → service → repository → SQLite.
How To Use It
# Clone and install
git clone https://github.com/moses-y/foldergram
cd foldergram
pnpm install
# Configure
cp .env.example .env # set GALLERY_ROOT, PORT, etc.
# Run server + client in dev
pnpm dev
# Or build and run with Docker
docker compose up -d
Setup is pnpm-based (pnpm-workspace.yaml). The Dockerfile and docker-compose.yml exist for container deployment. The server entry point is server/src/index.ts; the client dev server runs via Vite (client/vite.config.ts). No test command is wired into CI, despite 88 test files.
Real-World Use
A photographer with a NAS-mounted GALLERY_ROOT folder structure can run Foldergram in Docker, point it at the media directory, and get an Instagram-style feed for clients without uploading to a cloud service. The lazy-derivative mode (LAZY_DERIVATIVES=true) generates thumbnails on first request, which suits large libraries where eager processing would stall startup.
Code Health & Issues
Static analysis found 31 findings (8 high, 23 medium). Key issues:
- High – Hub modules:
client/src/types/api.ts(46 dependents) andserver/src/utils/image-utils.ts(28 dependents) — high blast radius on change. - High – Oversized files:
server/src/db/repositories.ts(2231 lines),scanner-service.ts,client/src/stores/collections.ts— hard to reason about. - High – Duplicated code: 707 repeated 6-line blocks across 114 files, including
AdminUnlockDialog.vueandAuthGate.vue. - Medium – High branching density in
exif-utils.ts,http.ts,csrf-protection.ts.
SDLC gaps: CI workflows (deploy-docs.yml, publish-ghcr.yml) never run the test suite, third-party actions aren't SHA-pinned, no Dependabot config, and the Docker base image (node:22-bookworm-slim) isn't digest-pinned.
The Bottom Line
Foldergram is a credible, self-contained gallery with a modern stack and real tests. The main risks are maintainability — oversized service files and hub modules — and CI that doesn't actually test. Worth using for small-to-medium local galleries; expect to refactor before extending heavily.