- JobRepository::claim_next() — atomic SELECT FOR UPDATE SKIP LOCKED + UPDATE status=processing in one query, no duplicate claims - ExecutePipelineHandler skips start() for already-claimed jobs - Sweep spawns N concurrent tasks via JoinSet, claims are fast+sequential, execution is slow+concurrent - Graceful shutdown: stop claiming, await all in-flight JoinSet tasks - WORKER_CONCURRENCY env (default: CPU cores) - DB_MAX_CONNECTIONS env (default: 20, was hardcoded 10) - VolumeFileResolver impl for InMemoryFileStorage (test fix)
K-Photos
Self-hosted media orchestrator and gallery. Alternative to Apple Photos, Google Photos, and Immich.
Philosophy
- Files are sacred — original file bytes and embedded metadata are never modified. Ever.
- No lock-in — all metadata exportable to standard formats (XMP, JSON). Stop using K-Photos and your tagged, organized file system remains intact.
- Virtual layer — all edits (dates, tags, albums, face regions) live in a separate layer. DB at runtime, sidecar exports for portability. Corrupt the layer? Rebuild from originals.
- Modular — core works without AI/ML. Face detection, classification, smart search are optional plugins.
- BYOS — bring your own storage. Local NAS, S3, GCS — the domain doesn't care.
Features
Photo Management
- Timeline — date-grouped photo grid sorted by EXIF capture date, infinite scroll, date scrubber for fast navigation
- Image viewer — fullscreen with zoom/pan/pinch (react-zoom-pan-pinch), keyboard nav, collapsible metadata sidebar (EXIF, camera, location)
- Albums — create, add/remove photos, asset picker dialog
- Upload — drag-drop with per-file progress, sequential upload through Next.js proxy
- Multi-select — select photos to bulk add to albums or delete
- Multi-volume — import photos from NAS, external drives, or cloud storage without copying
Safe Deletion
- Read-only volumes (NAS, archives): delete removes DB records + derivatives. Original files never touched.
- Writable volumes (uploads): soft-delete to trash with configurable grace period before permanent purge.
- Trash — view trashed photos, restore before purge.
TRASH_RETENTION_DAYS(default 30).
Admin
- Storage — register volumes + library paths, import library (one-click scan), delete
- Jobs — queue dashboard with filtering, pagination, error details, start/fail actions
- Plugins — list, enable/disable toggle, create
- Pipelines — list configured pipelines, create trigger-based processing chains
- Sidecars — detect changes, bulk export/import, per-asset conflict resolution
- Duplicates — view duplicate groups with thumbnails, resolve by picking keeper
Auth
- JWT access tokens + refresh token rotation
- Role-based access: first registered user auto-promoted to admin
- Admin section in sidebar, hidden for regular users
Architecture
Hexagonal / DDD with CQRS. Dependencies point inward:
Infrastructure (Axum, Postgres, NATS, S3)
-> Adapters (Controllers, Repos, Storage Providers)
-> Application (Commands / Queries)
-> Domain (Entities, Value Objects, Ports, Services)
Bounded Contexts
| Context | Purpose |
|---|---|
| Identity | Users, roles, RBAC permissions, groups, refresh tokens |
| Storage | Volumes, library paths, ingestion, quotas, BYOS |
| Catalog | Assets, metadata layers, stacks, derivatives, duplicates, visibility filtering |
| Organization | Albums, tags, collections (smart albums) |
| Sharing | Share scopes, targets, links, invite codes, visibility filters |
| Sidecar | XMP/JSON export, sync state, conflict resolution |
| Processing | Jobs, batches, plugins, pipelines, directory scanner |
Project Structure
crates/
domain/ pure Rust — entities, value objects, ports, services
application/ CQRS commands + queries with Arc<dyn Port> injection
api-types/ HTTP request/response DTOs with OpenAPI derives
adapters/
auth/ bcrypt + JWT
postgres/ repos, event store, migrations
storage/ local filesystem, volume-aware file resolver
exif/ EXIF metadata extraction
thumbnail/ derivative generation
sidecar/ XMP reader/writer
event-transport/ composite publisher (NATS + event store)
nats/ NATS JetStream transport
presentation/ axum handlers, routes, middleware
bootstrap/ config, DI wiring, API server entry point
worker/ background job runner (NATS consumer, sweep, trash purge)
k-photos-frontend/ Next.js 16 + shadcn + TanStack Query
app/(auth)/ login, register
app/(app)/ timeline, albums, trash, admin pages
components/ photo grid, image viewer, upload dialog, sidebars
hooks/ auth, timeline, albums, upload, admin hooks
lib/ API client, token helpers, types
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
yes | — | Postgres connection string |
JWT_SECRET |
yes | — | HMAC secret for JWT signing |
NATS_URL |
no | nats://localhost:4222 |
NATS server URL |
STORAGE_PATH |
no | ./data/media |
Local file storage root |
HOST |
no | 0.0.0.0 |
Bind address |
PORT |
no | 8000 |
Bind port |
CORS_ALLOWED_ORIGINS |
no | — | Comma-separated origins |
MAX_UPLOAD_BYTES |
no | 268435456 |
Max upload size (256 MiB) |
TRASH_RETENTION_DAYS |
no | 30 |
Days before trashed assets are permanently purged |
RUST_LOG |
no | info |
Log level filter |
Development
# prerequisites: postgres, nats-server, bun
# backend
cp .env.example .env # edit DATABASE_URL, JWT_SECRET
cargo run -p bootstrap # API server on :8000
cargo run -p worker # background job runner
# frontend
cd k-photos-frontend
bun install
bun run dev # Next.js on :3000 (proxies /api/v1 to :8000)
# tests
cargo test --workspace
Docker
docker compose up -d # postgres + nats
cargo run -p bootstrap
cargo run -p worker
License
Languages
Rust
67.7%
TypeScript
31.5%
CSS
0.5%
Dockerfile
0.2%