CODEMAP.md
PhotoPrism — Backend CODEMAP
Last Updated: March 8, 2026
Purpose
Quick Start
make depmake build-gomake lint-go (uses .golangci.yml; prints findings without failing) or run both stacks with make lint./photoprism startmake docker-builddocker compose up -ddocker compose logs -f --tail=100 photoprismExecutables & Entry Points
photoprism):
cmd/photoprism/photoprism.gointernal/commands/commands.go (array commands.PhotoPrism)internal/commands/catalog (DTOs and builders to enumerate commands/flags; Markdown renderer)internal/commands/start.go → server.Start (starts HTTP(S), workers, session cleanup)internal/server/start.go (compression, security, healthz, readiness, TLS/AutoTLS/unix socket)internal/server/routes.go (registers all v1 API groups + UI, WebDAV, sharing, .well-known)APIv1 = router.Group(conf.BaseUri("/api/v1"), Api(conf))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.go wires them into the app and subpackages like catalog emit 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, migrations
internal/entity/label*.go; reuse FindLabels(...), FindLabelIDs(...), and LabelSlugs(...) for homophone-aware exact-name/slug resolution instead of duplicating slug SQL in callers.internal/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.md and internal/ai/vision/ollama/README.md.internal/workers — background schedulers (index, vision, sync, meta, backup)internal/auth — ACL, sessions, OIDCinternal/service — cluster/portal, maps, hub, webdav
internal/service/webdav/README.mdPROPFIND Depth: infinity and falls back to iterative Depth: 1 traversal for incompatible servers.Files, Directories, Mkdir, Delete), while Upload and Download avoid total request deadlines and instead use connection-level safeguards.internal/event — logging, pub/sub, audit; canonical outcome tokens live in pkg/log/status (use helpers like status.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 from internal/*), e.g. pkg/clean, pkg/enum, pkg/fs, pkg/txt, pkg/http/headerTemplates & Static Assets
assets/templates/index.gohtml, which includes the splash markup from app.gohtml and the SPA loader from app.js.gohtml.assets/templates/auth.gohtml, which clears legacy/namespaced session keys and writes the session into the preferred namespaced browser store selected by the login UI toggle in frontend/src/page/auth/login.vue.assets/static/js/browser-check.js and is included via app.js.gohtml; it performs capability checks (Promise, fetch, AbortController, script.noModule, etc.) before the main bundle runs.pro/assets/templates/index.gohtml, plus/assets/templates/index.gohtml, and portal/assets/templates/index.gohtml, because those editions import the same partial.splash.gohtml renders the loading screen text while the bundle loads; styles are in frontend/src/css/splash.css.internal/server/routes_webapp.go. Handlers for sw.js, sw-scope-cleanup.js, and Workbox runtime files (/workbox-:hash) are defined there so service workers run under both the site root and a base URI; remember Gin’s :hash parameter excludes the .js suffix, so the handler/test matches the full filename manually.HTTP API
internal/api/*.go and are registered in internal/server/routes.go.make fmt-go swag-fmt && make swag.internal/api/swagger.json by hand./api/v1/... in every @Router annotation (match the group prefix).make swag-json runs a stabilization step (swaggerfix) removing duplicated enums for time.Duration; API uses integer nanoseconds for durations./api/v1/metrics (see internal/api/metrics.go) exposes Prometheus metrics, including cached filesystem/account usage derived from config.Usage(), registered user/guest totals, and portal cluster node counts when NodeRole=portal; the handler returns the standard Prometheus exposition content type (text/plain; version=0.0.4).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)./library/hidden for CE/Plus/Pro and /portal/admin/hidden for Portal) is implemented in internal/entity/search/photos.go:
frm.Hidden enforces photos.photo_quality = -1 and photos.deleted_at IS NULL.files.file_error = '') unless frm.Error is explicitly set.internal/entity/search/photos_results.go expose FileError (files.file_error) so clients can render hidden reasons without loading full file details first.Configuration & Flags
internal/config/options.go with yaml:"…" (for defaults.yml/options.yml), json:"…" (clients/API), and flag:"…" (CLI flags/env) tags.
json:"-" disables JSON processing to prevent values from being exposed through the API (see internal/api/config_options.go).yaml:"-" disables YAML processing; flag:"-" prevents ApplyCliContext() from assigning CLI values (flags/env variables) to a field, without affecting the flags in internal/config/flags.go.tags:"plus,pro" to control visibility (see internal/config/options_report.go logic).internal/config/flags.go (EnvVars(...))
internal/config/cli_flags_report.go + internal/config/report_sections.go → surfaced by photoprism show config-options --md/--jsoninternal/config/options_report.go + internal/config/report_sections.go → surfaced by photoprism show config-yaml --md/--jsoninternal/config/report.go → surfaced by photoprism show config (alias photoprism config --md).internal/commands/show_commands.go → surfaced by photoprism show commands (Markdown by default; --json alternative; --nested optional tree; --all includes hidden commands/flags; nested help subcommands omitted).defaults.yml < CLI/env < options.yml (global options rule). See Agent Tips in AGENTS.md.Config.SaveOptionsPatch(...) in internal/config/config.go for generic options.yml merge/write/reload.Config.SaveClusterOptionsUpdate(...) in internal/config/config_cluster.go for cluster metadata updates (ClusterUUID, NodeUUID, NodeClientID, DB fields, etc.).internal/config/config_db.go, server in config_server.go, TLS in config_tls.go, etc./api/v1/config (see internal/api/api_client_config.go).404 to prevent intermediary cache mix-ups between public and session-specific config payloads.internal/config/client_config.go (not a direct serialization of Options) plus extension values registered via config.Register in internal/config/extensions.go.UpdateClientConfig() to publish "config.updated" over websockets after changes (see internal/api/config_options.go and internal/api/config_settings.go).config.Register rather than exposing Options directly.$config.update() in frontend/src/app.js, complementing the websocket push.pro, flags hidden in CE): oidc-group-claim (default groups), oidc-group (required membership list), oidc-group-role (mapping GROUP=ROLE).internal/auth/oidc/groups.go normalizes IDs, detects Entra _claim_names overage, maps groups→roles, and enforces required membership in internal/api/oidc_redirect.go._claim_names.groups is present and no groups are returned, login fails when required groups are configured; Graph fetch is not implemented yet.Database & Migrations
github.com/jinzhu/gorm). No WithContext. Use db.Raw(stmt).Scan(&nop) for raw SQL.internal/entity/*.go and subpackages (query, search, sortby).internal/entity/migrate/* — run via config.MigrateDb(); CLI: photoprism migrate / photoprism migrations.internal/config/config_db.go chooses driver/DSN, sets gorm:table_options, then entity.InitDb(migrate.Opt(...)).AuthN/Z & Sessions
internal/entity/auth_session* and internal/auth/session/* (cleanup worker).
internal/entity/auth_session_jwt.go builds transient sessions from portal-issued JWTs; used by internal/api/api_auth_jwt.go when nodes authenticate portal requests.internal/auth/acl/* — roles, grants, scopes; use constants; avoid logging secrets, compare tokens constant‑time; for scope checks use acl.ScopePermits / ScopeAttrPermits instead of rolling your own parsing.internal/auth/oidc/*.Media Processing
internal/thumb/* and helpers in internal/photoprism/mediafile_thumbs.go.internal/meta/*.internal/ffmpeg/*.scripts/dist/install-libheif.sh; regenerate archives with make build-libheif-* (wraps scripts/dist/build-libheif.sh for each supported distro/arch) before publishing to dl.photoprism.app/dist/libheif/.internal/entity/folder.go keeps FindFolder(...) unscoped for create/index conflict handling, so a soft-deleted row cannot cause repeated insert/fail/not-found loops.internal/photoprism/index.go runs entity.ReconcileOriginalsFolderAlbums(...) only on forced rescans, after the file walk, so regular indexing stays lightweight while complete rescans repair stale/missing folder albums.Background Workers
internal/workers/*.go (index, vision, meta, sync, backup, share); started from internal/commands/start.go.internal/workers/auto/*.Cluster / Portal
internal/service/cluster/const.go (cluster.RoleInstance, cluster.RolePortal, cluster.RoleService).internal/service/cluster/node/* (HTTP to Portal; do not import Portal internals).
internal/service/cluster/registry/*, internal/service/cluster/provisioner/*./api/v1/cluster/theme; client/CLI installs theme only if missing or no app.js.portal/internal/portal (Portal defaults, flags, provisioning options, /i/* proxy router).specs/portal/README.md.Logging & Events
internal/event/*; event.Log is the shared logger.pkg/http/header/* — always prefer these in handlers and tests.Server Startup Flow (happy path)
photoprism start (CLI) → internal/commands/start.gointernal/server/start.go builds Gin engine, middleware, API group, templatesinternal/server/routes.go registers UI, WebDAV, sharing, well‑known, and all /api/v1/* routes/livez, /readyz availableCommon How‑Tos
Add a CLI command
internal/commands/<name>.go with a *cli.CommandPhotoPrism in internal/commands/commands.goRunWithTestContext from internal/commands/commands_test.go to avoid os.ExitAdd a REST endpoint
internal/api/<area>.go with Swagger annotationsinternal/server/routes.goapi.ClientIP(c), header.BearerToken(c), Abort* functionscount=100, max 1000, offset>=0) for list endpointsmake fmt-go swag-fmt && make swag; keep docs accuratego test ./internal/api -run <Name> and focused helpers (NewApiTest(), PerformRequest*)Add a config option
internal/config/options.gointernal/config/flags.go via EnvVars(...)config_server.go or topic file)rows in *config.Report() after the same option as in options.gooptions.yml and reload into memory (prefer Config.SaveOptionsPatch(...) and related config-owned helpers over ad-hoc YAML logic).pkg/fs.ConfigFilePath so .yml and .yaml stay interchangeable.internal/config/test.go helpers)Touch the DB schema
internal/entity/migrate/<dialect>/... and run go generate or make generate (runs go generate for all packages)migrate.Version usage via config_db.goTesting
make test (frontend + backend). Backend only: make test-go.go test ./internal/<pkg> -run <Name>.PHOTOPRISM_CLI=noninteractive or pass --yes to avoid prompts; use RunWithTestContext to prevent os.Exit.frontend/CODEMAP.md.hub.ApplyTestConfig()).assets/ folder, so avoid adding per-package PHOTOPRISM_ASSETS_PATH shims unless you have an unusual layout.Security & Hot Spots (Where to Look)
Zip extraction (path traversal prevention): pkg/fs/zip.go
safeJoin to reject absolute/volume paths and .. traversal; enforces per-file and total size limits.pkg/fs/zip_extra_test.go cover abs/volume/.. cases and limits.Force-aware Copy/Move and truncation-safe writes:
internal/photoprism/mediafile.go (MediaFile.Copy/Move with force).pkg/fs/copy.go, pkg/fs/move.go (use O_TRUNC to avoid trailing bytes).FFmpeg command builders and encoders:
internal/ffmpeg/transcode_cmd.go, internal/ffmpeg/remux.go.internal/ffmpeg/{apple,intel,nvidia,vaapi,v4l}/avc.go.PHOTOPRISM_FFMPEG_ENCODER; otherwise assert command strings and negative paths.libvips thumbnails:
internal/thumb/vips.go (VipsInit, VipsRotate, export params).internal/thumb/sizes.go, internal/thumb/names.go, internal/thumb/filter.go; face/marker crop helpers live in internal/thumb/crop (e.g., ParseThumb, IsCroppedThumb).Safe HTTP downloader:
pkg/http/safe (Download, Options).0600 + rename.internal/thumb/avatar.SafeDownload applies stricter defaults (15s, 10 MiB, AllowPrivate=false, image‑focused Accept).go test ./pkg/http/safe -count=1 (includes redirect SSRF cases); avatars: go test ./internal/thumb/avatar -count=1.CDN guards for credential flows:
POST /api/v1/cluster/nodes/register also rejects CDN-marked requests to avoid caching responses that may contain bootstrap secrets.Performance & Limits
count=100 (max 1000); set Cache-Control: no-store for secrets.Conventions & Rules of Thumb
pkg/* must not import internal/*.pkg/http/header over string literals.t.TempDir() and env like PHOTOPRISM_STORAGE_PATH.NodeUUID; exposed as UUID in API/CLI). The OAuth client ID (NodeClientID, exposed as ClientID) 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
github.com/photoprism/photoprism/pkg/fs permission variables when creating files/dirs:
fs.ModeDir (0o755 with umask), fs.ModeFile (0o644 with umask), fs.ModeConfigFile (0o664), fs.ModeSecretFile (0o600), fs.ModeBackupFile (0o600).io/fs mode bits as permission arguments. When importing stdlib io/fs, alias it (iofs/gofs) to avoid fs.* collisions with our package.filepath.Join for filesystem paths across platforms; use path.Join for URLs only.Cluster Registry & Provisioner Cheatsheet
{uuid}, Registry Get/Delete/RotateSecret by UUID; explicit FindByClientID exists for OAuth.uuid required; clientId optional; database metadata includes driver.cluster_d<hmac11>cluster_u<hmac11>
HMAC is base32 of ClusterUUID+NodeUUID; drivers currently mysql|mariadb.BuildDSN(driver, host, port, user, pass, name); warns and falls back to MySQL format for unsupported drivers.path/to/pkg/<file>.go, add tests in path/to/pkg/<file>_test.go (create if missing). For the same function, group related cases as t.Run(...) sub-tests (table-driven where helpful) and name each subtest string in PascalCase.Database (not db) with Name, User, Driver, RotatedAt.RotatedAt.Secrets.ClientSecret; the CLI persists it under config NodeClientSecret.AdvertiseUrl and Database; non-admin responses are redacted by default.photoprism cluster register supports --site-url and --advertise-url. Both values are always forwarded to the Portal; SiteUrl no longer depends on being different from the advertised URL.config.ShouldAutoRotateDatabase() and is shared by both the CLI and node bootstrap.Frequently Touched Files (by topic)
cmd/photoprism/photoprism.go, internal/commands/commands.gointernal/server/start.go, internal/server/routes.go, middleware in internal/server/*.gointernal/api/*.go (plus docs.go for package docs)internal/config/* (flags.go, config_db.go, config_server.go, options.go)internal/entity/*.go, internal/entity/query/*internal/entity/migrate/*internal/workers/*internal/service/cluster/*
internal/service/cluster/theme/version.go exposes DetectVersion, used by bootstrap, CLI, and API handlers to compare portal vs node theme revisions (prefers fs.VersionTxtFile, falls back to app.js mtime).AppName, AppVersion, and Theme with clean.TypeUnicode; defaults for app metadata come from config.About() / config.Version(). cluster.RegisterResponse now includes a Theme hint when the portal has a newer bundle so nodes can decide whether to download immediately.pkg/http/header/*Downloads (CLI) & yt-dlp helpers
internal/commands/download.go (flags, defaults, examples)internal/commands/download_impl.go (testable implementation used by CLI)internal/photoprism/dl/options.go (arg wiring; FFmpegPostArgs hook 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 (CreatedFromInfo fallback; RemuxOptionsFromInfo)internal/photoprism/get/import.go (work pool)internal/photoprism/import_options.go (ImportOptionsMove/Copy)go test ./internal/photoprism/dl -run 'Options|Created|PostprocessorArgs' -count=1go test ./internal/commands -run 'DownloadImpl|HelpFlags' -count=1FFmpegBin = "/bin/false", Settings.Index.Convert=false in tests.--dump-single-json, creates a file and prints path for --print.YTDLP_DUMMY_CONTENT) or dest.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 in internal/api/swagger.jsonmake fmt-go swag-fmt — format Go code and Swagger annotationsSee Also
specs/dev/backend-testing.md, specs/dev/api-docs-swagger.md, specs/portal/README.mdGo Internal Import Rule
internal/...; the Go toolchain blocks importing internal/ packages from directories such as /tmp, so use a disposable path like internal/tmp/ when you need scratch space.Fast Test Recipes
go test ./pkg/fs -run 'Copy|Move|Unzip' -count=1go test ./pkg/media/... -count=1go test ./internal/thumb/... -count=1go test ./internal/ffmpeg -run 'Remux|Transcode|Extract' -count=1