22 KiB
PhotoPrism — Backend CODEMAP
Last Updated: November 22, 2025
Purpose
- Give agents and contributors a fast, reliable map of where things live and how they fit together, so you can add features, fix bugs, and write tests without spelunking.
- Sources of truth: prefer Makefile targets and the Developer Guide linked in AGENTS.md.
Quick Start
- Inside dev container (recommended):
- Install deps:
make dep - Build backend:
make build-go - Lint Go (golangci-lint):
make lint-go(uses.golangci.yml; prints findings without failing) or run both stacks withmake lint - Run server:
./photoprism start - Open: http://localhost:2342/ or https://app.localssl.dev/ (Traefik required)
- Install deps:
- On host (manages Docker):
- Build image:
make docker-build - Start services:
docker compose up -d - Logs:
docker compose logs -f --tail=100 photoprism
- Build image:
Executables & Entry Points
- CLI app (binary name across docs/images is
photoprism):- Main:
cmd/photoprism/photoprism.go - Commands registry:
internal/commands/commands.go(arraycommands.PhotoPrism) - Catalog helpers:
internal/commands/catalog(DTOs and builders to enumerate commands/flags; Markdown renderer)
- Main:
- Web server:
- Startup:
internal/commands/start.go→server.Start(starts HTTP(S), workers, session cleanup) - HTTP server:
internal/server/start.go(compression, security, healthz, readiness, TLS/AutoTLS/unix socket) - Routes:
internal/server/routes.go(registers all v1 API groups + UI, WebDAV, sharing, .well-known) - API group:
APIv1 = router.Group(conf.BaseUri("/api/v1"), Api(conf))
- Startup:
High-Level Package Map (Go)
internal/api— Gin handlers and Swagger annotations; only glue, no business logicinternal/commands— CLI command definitions and orchestration (start,index,import,migrate, etc.);commands.gowires them into the app and subpackages likecatalogemit CLI documentation.internal/server— HTTP server, middleware, routing, static/ui/webdavinternal/config— configuration, flags/env/options, client config, DB init/migrateinternal/entity— GORM v1 models, queries, search helpers, migrationsinternal/photoprism— core domain logic (indexing, import, faces, thumbnails, cleanup)internal/ai/vision— multi-engine computer vision pipeline (models, adapters, schema). Adapter docs:internal/ai/vision/openai/README.mdandinternal/ai/vision/ollama/README.md.internal/workers— background schedulers (index, vision, sync, meta, backup)internal/auth— ACL, sessions, OIDCinternal/service— cluster/portal, maps, hub, webdavinternal/event— logging, pub/sub, audit; canonical outcome tokens live inpkg/log/status(use helpers likestatus.Error(err)when the sanitized message should be the outcome). Docs:internal/event/README.md.internal/ffmpeg,internal/thumb,internal/meta,internal/form,internal/mutex— media, thumbs, metadata, forms, coordination. Docs:internal/ffmpeg/README.md,internal/meta/README.md.pkg/*— reusable utilities (must never import frominternal/*), e.g.pkg/clean,pkg/enum,pkg/fs,pkg/txt,pkg/http/header
Templates & Static Assets
- Entry HTML lives in
assets/templates/index.gohtml, which includes the splash markup fromapp.gohtmland the SPA loader fromapp.js.gohtml. - The browser check logic resides in
assets/static/js/browser-check.jsand is included viaapp.js.gohtml; it performs capability checks (Promise, fetch, AbortController,script.noModule, etc.) before the main bundle runs. Update this file (and the partial) in lockstep with the templates in private repos (pro/assets/templates/index.gohtml,plus/assets/templates/index.gohtml) because they import the same partial, and keep the<script>order so the check is executed first. splash.gohtmlrenders the loading screen text while the bundle loads; styles are infrontend/src/css/splash.css.- When adjusting browser support messaging, update both the loader partial and splash styles so the warning message stays consistent across editions.
- Service worker routes live in
internal/server/routes_webapp.go. The helper that serves Workbox runtime files (/workbox-:hash) sits there as well so service workers run under both the site root and a base URI; remember Gin’s:hashparameter excludes the.jssuffix, so the handler/test matches the full filename manually.
HTTP API
- Handlers live in
internal/api/*.goand are registered ininternal/server/routes.go. - Annotate new endpoints in handler files; generate docs with:
make fmt-go swag-fmt && make swag. - Do not edit
internal/api/swagger.jsonby hand. - Swagger notes:
- Use full
/api/v1/...in every@Routerannotation (match the group prefix). - Annotate only public handlers; skip internal helpers to avoid stray generic paths.
make swag-jsonruns a stabilization step (swaggerfix) removing duplicated enums fortime.Duration; API uses integer nanoseconds for durations.
- Use full
/api/v1/metrics(seeinternal/api/metrics.go) exposes Prometheus metrics, including cached filesystem/account usage derived fromconfig.Usage(), registered user/guest totals, and portal cluster node counts whenNodeRole=portal; the handler returns the standard Prometheus exposition content type (text/plain; version=0.0.4).- Common groups in
routes.go: sessions, OAuth/OIDC, config, users, services, thumbnails, video, downloads/zip, index/import, photos/files/labels/subjects/faces, batch ops, cluster, technical (metrics, status, echo).
Configuration & Flags
- Options struct:
internal/config/options.gowithyaml:"…"(fordefaults.yml/options.yml),json:"…"(clients/API), andflag:"…"(CLI flags/env) tags.- For secrets/internals:
json:"-"disables JSON processing to prevent values from being exposed through the API (seeinternal/api/config_options.go). - If needed:
yaml:"-"disables YAML processing;flag:"-"preventsApplyCliContext()from assigning CLI values (flags/env variables) to a field, without affecting the flags ininternal/config/flags.go. - Annotations may include edition tags like
tags:"plus,pro"to control visibility (seeinternal/config/options_report.gologic).
- For secrets/internals:
- Global flags/env:
internal/config/flags.go(EnvVars(...))- Available flags/env:
internal/config/cli_flags_report.go+internal/config/report_sections.go→ surfaced byphotoprism show config-options --md/--json - YAML options mapping:
internal/config/options_report.go+internal/config/report_sections.go→ surfaced byphotoprism show config-yaml --md/--json - Report current values:
internal/config/report.go→ surfaced byphotoprism show config(aliasphotoprism config --md). - CLI commands catalog:
internal/commands/show_commands.go→ surfaced byphotoprism show commands(Markdown by default;--jsonalternative;--nestedoptional tree;--allincludes hidden commands/flags; nestedhelpsubcommands omitted).
- Available flags/env:
- Precedence:
defaults.yml< CLI/env <options.yml(global options rule). See Agent Tips inAGENTS.md. - Getters are grouped by topic, e.g. DB in
internal/config/config_db.go, server inconfig_server.go, TLS inconfig_tls.go, etc. - Client Config (read-only)
- Endpoint: GET
/api/v1/config(seeinternal/api/api_client_config.go). - Assembly: Built from
internal/config/client_config.go(not a direct serialization of Options) plus extension values registered viaconfig.Registerininternal/config/extensions.go. - Updates: Back-end calls
UpdateClientConfig()to publish "config.updated" over websockets after changes (seeinternal/api/config_options.goandinternal/api/config_settings.go). - ACL/mode aware: Values are filtered by user/session and may differ for public vs. authenticated users.
- Don’t expose secrets: Treat it as client-visible; avoid sensitive data. To add fields, extend client values via
config.Registerrather than exposing Options directly. - Refresh cadence: The web UI (non‑mobile) also polls for updates every 10 minutes via
$config.update()infrontend/src/app.js, complementing the websocket push.
- Endpoint: GET
- OIDC Groups (Pro-Only)
- Config options (tagged
pro, flags hidden in CE):oidc-group-claim(defaultgroups),oidc-group(required membership list),oidc-group-role(mappingGROUP=ROLE). - Parsing/helpers:
internal/auth/oidc/groups.gonormalizes IDs, detects Entra_claim_namesoverage, maps groups→roles, and enforces required membership ininternal/api/oidc_redirect.go. - Overage: if
_claim_names.groupsis present and no groups are returned, login fails when required groups are configured; Graph fetch is not implemented yet.
- Config options (tagged
Database & Migrations
- Driver: GORM v1 (
github.com/jinzhu/gorm). NoWithContext. Usedb.Raw(stmt).Scan(&nop)for raw SQL. - Entities and helpers:
internal/entity/*.goand subpackages (query,search,sortby). - Migrations engine:
internal/entity/migrate/*— run viaconfig.MigrateDb(); CLI:photoprism migrate/photoprism migrations. - DB init/migrate flow:
internal/config/config_db.gochooses driver/DSN, setsgorm:table_options, thenentity.InitDb(migrate.Opt(...)).
AuthN/Z & Sessions
- Session model and cache:
internal/entity/auth_session*andinternal/auth/session/*(cleanup worker).internal/entity/auth_session_jwt.gobuilds transient sessions from portal-issued JWTs; used byinternal/api/api_auth_jwt.gowhen nodes authenticate portal requests.
- ACL:
internal/auth/acl/*— roles, grants, scopes; use constants; avoid logging secrets, compare tokens constant‑time; for scope checks useacl.ScopePermits/ScopeAttrPermitsinstead of rolling your own parsing. - OIDC:
internal/auth/oidc/*.
Media Processing
- Thumbnails:
internal/thumb/*and helpers ininternal/photoprism/mediafile_thumbs.go. - Metadata:
internal/meta/*. - FFmpeg integration:
internal/ffmpeg/*. - HEIF tooling: distribution binaries live under
scripts/dist/install-libheif.sh; regenerate archives withmake build-libheif-*(wrapsscripts/dist/build-libheif.shfor each supported distro/arch) before publishing todl.photoprism.app/dist/libheif/.
Background Workers
- Scheduler and workers:
internal/workers/*.go(index, vision, meta, sync, backup, share); started frominternal/commands/start.go. - Auto indexer:
internal/workers/auto/*.
Cluster / Portal
- Node types:
internal/service/cluster/const.go(cluster.RoleApp,cluster.RolePortal,cluster.RoleService). - Node bootstrap & registration:
internal/service/cluster/node/*(HTTP to Portal; do not import Portal internals).- Registration now retries once on 401/403 by rotating the node client secret with the join token and persists the new credentials (falling back to in-memory storage if the secrets directory is read-only).
- Theme sync logs explicitly when refresh/rotation occurs so operators can trace credential churn in standard log levels.
- Registry/provisioner:
internal/service/cluster/registry/*,internal/service/cluster/provisioner/*. - Theme endpoint (server): GET
/api/v1/cluster/theme; client/CLI installs theme only if missing or noapp.js. - See specs cheat sheet:
specs/portal/README.md.
Logging & Events
- Logger and event hub:
internal/event/*;event.Logis the shared logger. - HTTP headers/constants:
pkg/http/header/*— always prefer these in handlers and tests.
Server Startup Flow (happy path)
photoprism start(CLI) →internal/commands/start.go- Config init, DB init/migrate, session cleanup worker
internal/server/start.gobuilds Gin engine, middleware, API group, templatesinternal/server/routes.goregisters UI, WebDAV, sharing, well‑known, and all/api/v1/*routes- Workers and auto‑index start; health endpoints
/livez,/readyzavailable
Common How‑Tos
-
Add a CLI command
- Create
internal/commands/<name>.gowith a*cli.Command - Add it to
PhotoPrismininternal/commands/commands.go - Tests: prefer
RunWithTestContextfrominternal/commands/commands_test.goto avoidos.Exit
- Create
-
Add a REST endpoint
- Create handler in
internal/api/<area>.gowith Swagger annotations - Register it in
internal/server/routes.go - Use helpers:
api.ClientIP(c),header.BearerToken(c),Abort*functions - Validate pagination bounds (default
count=100, max1000,offset>=0) for list endpoints - Run
make fmt-go swag-fmt && make swag; keep docs accurate - Tests:
go test ./internal/api -run <Name>and focused helpers (NewApiTest(),PerformRequest*)
- Create handler in
-
Add a config option
- Add field with tags to
internal/config/options.go - Register CLI flag/env in
internal/config/flags.goviaEnvVars(...) - Expose a getter (e.g., in
config_server.goor topic file) - Append to
rowsin*config.Report()after the same option as inoptions.go - If value must persist, write back to
options.ymland reload into memory - When you need the path to defaults/options/settings files, call
pkg/fs.ConfigFilePathso.ymland.yamlstay interchangeable. - Tests: cover CLI/env/file precedence (see
internal/config/test.gohelpers)
- Add field with tags to
-
Touch the DB schema
- Use GORM auto-migration, or add a custom migration in
internal/entity/migrate/<dialect>/...and rungo generateormake generate(runsgo generatefor all packages) - Bump/review version gates in
migrate.Versionusage viaconfig_db.go - Tests: run against SQLite by default; for MySQL cases, gate appropriately
- Use GORM auto-migration, or add a custom migration in
Testing
- Full suite:
make test(frontend + backend). Backend only:make test-go. - Focused packages:
go test ./internal/<pkg> -run <Name>. - CLI tests:
PHOTOPRISM_CLI=noninteractiveor pass--yesto avoid prompts; useRunWithTestContextto preventos.Exit. - SQLite DSN in tests is per‑suite (not empty). Clean up files if you capture the DSN.
- Frontend unit tests via Vitest are separate; see
frontend/CODEMAP.md. - Config helpers automatically disable Hub service calls for tests (
hub.ApplyTestConfig()). - Test configs auto-discover the repo
assets/folder, so avoid adding per-packagePHOTOPRISM_ASSETS_PATHshims unless you have an unusual layout.
Security & Hot Spots (Where to Look)
-
Zip extraction (path traversal prevention):
pkg/fs/zip.go- Uses
safeJointo reject absolute/volume paths and..traversal; enforces per-file and total size limits. - Tests:
pkg/fs/zip_extra_test.gocover abs/volume/.. cases and limits.
- Uses
-
Force-aware Copy/Move and truncation-safe writes:
- App helpers:
internal/photoprism/mediafile.go(MediaFile.Copy/Movewithforce). - Utils:
pkg/fs/copy.go,pkg/fs/move.go(useO_TRUNCto avoid trailing bytes).
- App helpers:
-
FFmpeg command builders and encoders:
- Core:
internal/ffmpeg/transcode_cmd.go,internal/ffmpeg/remux.go. - Encoders (string builders only):
internal/ffmpeg/{apple,intel,nvidia,vaapi,v4l}/avc.go. - Tests guard HW runs with
PHOTOPRISM_FFMPEG_ENCODER; otherwise assert command strings and negative paths.
- Core:
-
libvips thumbnails:
- Pipeline:
internal/thumb/vips.go(VipsInit, VipsRotate, export params). - Sizes & names:
internal/thumb/sizes.go,internal/thumb/names.go,internal/thumb/filter.go; face/marker crop helpers live ininternal/thumb/crop(e.g.,ParseThumb,IsCroppedThumb).
- Pipeline:
-
Safe HTTP downloader:
- Shared utility:
pkg/http/safe(Download,Options). - Protections: scheme allow‑list (http/https), pre‑DNS + per‑redirect hostname/IP validation, final peer IP check, size and timeout enforcement, temp file
0600+ rename. - Avatars: wrapper
internal/thumb/avatar.SafeDownloadapplies stricter defaults (15s, 10 MiB,AllowPrivate=false, image‑focusedAccept). - Tests:
go test ./pkg/http/safe -count=1(includes redirect SSRF cases); avatars:go test ./internal/thumb/avatar -count=1.
- Shared utility:
Performance & Limits
- Prefer existing caches/workers/batching as per Makefile and code.
- When adding list endpoints, default
count=100(max1000); setCache-Control: no-storefor secrets.
Conventions & Rules of Thumb
- Respect package boundaries: code in
pkg/*must not importinternal/*. - Prefer constants/helpers from
pkg/http/headerover string literals. - Never log secrets; compare tokens constant‑time.
- Don’t import Portal internals from cluster instance/service bootstraps; use HTTP.
- Prefer small, hermetic unit tests; isolate filesystem paths with
t.TempDir()and env likePHOTOPRISM_STORAGE_PATH. - Cluster nodes: identify by UUID v7 (internally stored as
NodeUUID; exposed asUUIDin API/CLI). The OAuth client ID (NodeClientID, exposed asClientID) is for OAuth only. Registry lookups and CLI commands accept UUID, ClientID, or DNS-label name (priority in that order).
Filesystem Permissions & io/fs Aliasing
- Use
github.com/photoprism/photoprism/pkg/fspermission variables when creating files/dirs:fs.ModeDir(0o755 with umask),fs.ModeFile(0o644 with umask),fs.ModeConfigFile(0o664),fs.ModeSecretFile(0o600),fs.ModeBackupFile(0o600).
- Do not use stdlib
io/fsmode bits as permission arguments. When importing stdlibio/fs, alias it (iofs/gofs) to avoidfs.*collisions with our package. - Prefer
filepath.Joinfor filesystem paths across platforms; usepath.Joinfor URLs only.
Cluster Registry & Provisioner Cheatsheet
- UUID‑first everywhere: API paths
{uuid}, RegistryGet/Delete/RotateSecretby UUID; explicitFindByClientIDexists for OAuth. - Node/DTO fields:
uuidrequired;clientIdoptional; database metadata includesdriver. - Provisioner naming (no slugs):
- database:
cluster_d<hmac11> - username:
cluster_u<hmac11>HMAC is base32 of ClusterUUID+NodeUUID; drivers currentlymysql|mariadb.
- database:
- DSN builder:
BuildDSN(driver, host, port, user, pass, name); warns and falls back to MySQL format for unsupported drivers. - Go tests live beside sources: for
path/to/pkg/<file>.go, add tests inpath/to/pkg/<file>_test.go(create if missing). For the same function, group related cases ast.Run(...)sub-tests (table-driven where helpful) and name each subtest string in PascalCase. - Public API and internal registry DTOs use normalized field names:
Database(notdb) withName,User,Driver,RotatedAt.- Node-level rotation timestamps use
RotatedAt. - Registration returns
Secrets.ClientSecret; the CLI persists it under configNodeClientSecret. - Admin responses may include
AdvertiseUrlandDatabase; non-admin responses are redacted by default.
- Cluster CLI highlights:
photoprism cluster registersupports--site-urland--advertise-url. Both values are always forwarded to the Portal;SiteUrlno longer depends on being different from the advertised URL.- Automatic MariaDB credential rotation logic now lives in
config.ShouldAutoRotateDatabase()and is shared by both the CLI and node bootstrap.
Frequently Touched Files (by topic)
- CLI wiring:
cmd/photoprism/photoprism.go,internal/commands/commands.go - Server:
internal/server/start.go,internal/server/routes.go, middleware ininternal/server/*.go - API handlers:
internal/api/*.go(plusdocs.gofor package docs) - Config:
internal/config/*(flags.go,config_db.go,config_server.go,options.go) - Entities & queries:
internal/entity/*.go,internal/entity/query/* - Migrations:
internal/entity/migrate/* - Workers:
internal/workers/* - Cluster:
internal/service/cluster/*- Theme support:
internal/service/cluster/theme/version.goexposesDetectVersion, used by bootstrap, CLI, and API handlers to compare portal vs node theme revisions (prefersfs.VersionTxtFile, falls back toapp.jsmtime). - Registration sanitizes
AppName,AppVersion, andThemewithclean.TypeUnicode; defaults for app metadata come fromconfig.About()/config.Version().cluster.RegisterResponsenow includes aThemehint when the portal has a newer bundle so nodes can decide whether to download immediately.
- Theme support:
- Headers:
pkg/http/header/*
Downloads (CLI) & yt-dlp helpers
- CLI command & core:
internal/commands/download.go(flags, defaults, examples)internal/commands/download_impl.go(testable implementation used by CLI)
- yt-dlp wrappers:
internal/photoprism/dl/options.go(arg wiring;FFmpegPostArgshook for--postprocessor-args)internal/photoprism/dl/info.go(metadata discovery)internal/photoprism/dl/file.go(file method with--output/--print)internal/photoprism/dl/meta.go(CreatedFromInfofallback;RemuxOptionsFromInfo)
- Importer:
internal/photoprism/get/import.go(work pool)internal/photoprism/import_options.go(ImportOptionsMove/Copy)
- Testing hints:
- Fast loops:
go test ./internal/photoprism/dl -run 'Options|Created|PostprocessorArgs' -count=1 - CLI only:
go test ./internal/commands -run 'DownloadImpl|HelpFlags' -count=1 - Disable ffmpeg when not needed: set
FFmpegBin = "/bin/false",Settings.Index.Convert=falsein tests. - Stub yt-dlp: shell script that prints JSON for
--dump-single-json, creates a file and prints path for--print. - Avoid importer dedup: vary file bytes (e.g.,
YTDLP_DUMMY_CONTENT) or dest.
- Fast loops:
Useful Make Targets (selection)
make help— list targetsmake dep— install Go/JS deps in containermake build-go— build backendmake test-go— backend tests (SQLite)make swag— generate Swagger JSON ininternal/api/swagger.jsonmake fmt-go swag-fmt— format Go code and Swagger annotations
See Also
- AGENTS.md (repository rules and tips for agents)
- Developer Guide (Setup/Tests/API) — links in AGENTS.md → Sources of Truth
- Specs:
specs/dev/backend-testing.md,specs/dev/api-docs-swagger.md,specs/portal/README.md
Go Internal Import Rule
- Keep temporary Go helpers inside
internal/...; the Go toolchain blocks importinginternal/packages from directories such as/tmp, so use a disposable path likeinternal/tmp/when you need scratch space.
Fast Test Recipes
- Filesystem + archives (fast):
go test ./pkg/fs -run 'Copy|Move|Unzip' -count=1 - Media helpers (fast):
go test ./pkg/media/... -count=1 - Thumbnails (libvips, moderate):
go test ./internal/thumb/... -count=1 - FFmpeg command builders (moderate):
go test ./internal/ffmpeg -run 'Remux|Transcode|Extract' -count=1