14 KiB
PhotoPrism® Repository Guidelines
Purpose
This file tells automated coding agents (and humans) where to find the single sources of truth for building, testing, and contributing to PhotoPrism. Learn more: https://agents.md/
Sources of Truth
- Makefile targets (always prefer existing targets): https://github.com/photoprism/photoprism/blob/develop/Makefile
- Developer Guide – Setup: https://docs.photoprism.app/developer-guide/setup/
- Developer Guide – Tests: https://docs.photoprism.app/developer-guide/tests/
- Contributing: https://github.com/photoprism/photoprism/blob/develop/CONTRIBUTING.md
- Security: https://github.com/photoprism/photoprism/blob/develop/SECURITY.md
- REST API: https://docs.photoprism.dev/ (Swagger), https://docs.photoprism.app/developer-guide/api/ (Docs)
- Developer Cheatsheet – Portal & Cluster: specs/portal/README.md
- Backend (Go) Testing Guide: specs/dev/backend-testing.md
Project Structure & Languages
- Backend: Go (
internal/
,pkg/
,cmd/
) + MariaDB/SQLite- Package boundaries: Code in
pkg/*
MUST NOT import frominternal/*
. - If you need access to config/entity/DB, put new code in a package under
internal/
instead ofpkg/
.
- Package boundaries: Code in
- Frontend: Vue 3 + Vuetify 3 (
frontend/
) - Docker/compose for dev/CI; Traefik is used for local TLS (
*.localssl.dev
)
Agent Runtime (Host vs Container)
Agents MAY run either:
- Inside the Development Environment container (recommended for least privilege).
- On the host (outside Docker), in which case the agent MAY start/stop the Dev Environment as needed.
Detecting the environment (agent logic)
Agents SHOULD detect the runtime and choose commands accordingly:
- Inside container if one of the following is true:
- File exists:
/.dockerenv
- Project path equals (or is a direct child of):
/go/src/github.com/photoprism/photoprism
- File exists:
Examples
Bash:
if [ -f "/.dockerenv" ] || [ -d "/go/src/github.com/photoprism/photoprism/.git" ]; then
echo "container"
else
echo "host"
fi
Node.js:
const fs = require("fs");
const inContainer = fs.existsSync("/.dockerenv");
const inDevPath = fs.existsSync("/go/src/github.com/photoprism/photoprism/.git");
console.log(inContainer || inDevPath ? "container" : "host");
Agent installation and invocation
-
Inside container: Prefer running agents via
npm exec
(no global install), for example:npm exec --yes <agent-binary> -- --help
- Or use
npx <agent-binary> ...
- If the agent is distributed via npm and must be global, install inside the container only:
npm install -g <agent-npm-package>
- Replace
<agent-binary>
/<agent-npm-package>
with the names from the agent’s official docs.
-
On host: Use the vendor’s recommended install for your OS. Ensure your agent runs from the repository root so it can discover
AGENTS.md
and project files.
Build & Run (local)
-
Run
make help
to see common targets (or open theMakefile
). -
Host mode (agent runs on the host; agent MAY manage Docker lifecycle):
- Build local dev image (once):
make docker-build
- Start services:
docker compose up
(add-d
to start in the background) - Follow live app logs:
docker compose logs -f --tail=100 photoprism
(Ctrl+C to stop)- All services:
docker compose logs -f --tail=100
- Last 10 minutes only:
docker compose logs -f --since=10m photoprism
- Plain output (easier to copy):
docker compose logs -f --no-log-prefix --no-color photoprism
- All services:
- Execute a single command in the app container:
docker compose exec photoprism <command>
- Example:
docker compose exec photoprism ./photoprism help
- Why
./photoprism
? It runs the locally built binary in the project directory. - Run as non-root to avoid root-owned files on bind mounts:
docker compose exec -u "$(id -u):$(id -g)" photoprism <command>
- Durable alternative: set the service user or
PHOTOPRISM_UID
/PHOTOPRISM_GID
incompose.yaml
; if you hit issues, runmake fix-permissions
.
- Example:
- Open a terminal session in the app container:
make terminal
- Stop everything when done:
docker compose --profile=all down --remove-orphans
(make down
does the same)
- Build local dev image (once):
-
Container mode (agent runs inside the app container):
- Install deps:
make dep
- Build frontend/backend:
make build-js
andmake build-go
- Watch frontend changes (auto-rebuild):
make watch-js
- Or run directly:
cd frontend && npm run watch
- Tips: refresh the browser to see changes; running the watcher outside the container can be faster on non-Linux hosts; stop with Ctrl+C
- Or run directly:
- Start the PhotoPrism server:
./photoprism start
- Open http://localhost:2342/ (HTTP)
- Or https://app.localssl.dev/ (HTTPS via Traefik reverse proxy)
- Only if Traefik is running and the dev compose labels are active
- Labels for
*.localssl.dev
are defined in the dev compose files, e.g. https://github.com/photoprism/photoprism/blob/develop/compose.yaml
- Do not use the Docker CLI inside the container; starting/stopping services requires host Docker access.
- Install deps:
Note: Across our public documentation, official images, and in production, the command-line interface (CLI) name is photoprism
. Other PhotoPrism binary names are only used in development builds for side-by-side comparisons of the Community Edition (CE) with PhotoPrism Plus (photoprism-plus
) and PhotoPrism Pro (photoprism-pro
).
Tests
- From within the Development Environment:
- Full unit test suite:
make test
(runs backend and frontend tests) - Test frontend/backend:
make test-js
andmake test-go
- Go packages:
go test
(all tests) orgo test -run <name>
(specific tests only)
- Full unit test suite:
- Frontend unit tests are driven by Vitest; see scripts in
frontend/package.json
- Vitest watch/coverage:
make vitest-watch
andmake vitest-coverage
- Vitest watch/coverage:
- Acceptance tests: use the
acceptance-*
targets in theMakefile
CLI Testing Gotchas (Go)
- Exit codes and
os.Exit
:urfave/cli
callsos.Exit(code)
when a command returnscli.Exit(...)
, which will terminatego test
abruptly (often after logs likehttp 401:
).- Use the test helper
RunWithTestContext
(ininternal/commands/commands_test.go
) which temporarily overridescli.OsExiter
so the process doesn’t exit; you still receive the error to assertExitCoder
. - If you only need to assert the exit code and don’t need printed output, you can invoke
cmd.Action(ctx)
directly and checkerr.(cli.ExitCoder).ExitCode()
.
- Non‑interactive mode: set
PHOTOPRISM_CLI=noninteractive
and/or pass--yes
to avoid prompts that block tests and CI. - SQLite DSN in tests:
config.NewTestConfig("<pkg>")
defaults to SQLite with a per‑suite DSN like.<pkg>.db
. Don’t assert an empty DSN for SQLite.- Clean up any per‑suite SQLite files in tests with
t.Cleanup(func(){ _ = os.Remove(dsn) })
if you capture the DSN.
Code Style & Lint
- Go: run
make fmt-go swag-fmt
to reformat the backend code + Swagger annotations (seeMakefile
for additional targets)- Doc comments for packages and exported identifiers must be complete sentences that begin with the name of the thing being described and end with a period.
- For short examples inside comments, indent code rather than using backticks; godoc treats indented blocks as preformatted.
- JS/Vue: use the lint/format scripts in
frontend/package.json
(ESLint + Prettier) - All added code and tests must be formatted according to our standards.
Safety & Data
- Never commit secrets, local configurations, or cache files. Use environment variables or a local
.env
.- Ensure
.env
and.local
are ignored in.gitignore
and.dockerignore
.
- Ensure
- Prefer using existing caches, workers, and batching strategies referenced in code and
Makefile
. Consider memory/CPU impact; suggest benchmarks or profiling only when justified. - Do not run destructive commands against production data. Prefer ephemeral volumes and test fixtures when running acceptance tests.
- Examples assume a Linux/Unix shell. For Windows specifics, see the Developer Guide FAQ: https://docs.photoprism.app/developer-guide/faq/#can-your-development-environment-be-used-under-windows
If anything in this file conflicts with the Makefile
or the Developer Guide, the Makefile
and the documentation win. When unsure, ask for clarification before proceeding.
Agent Tips
Backend Development
The following conventions summarize the insights gained when adding new configuration options, API endpoints, and related tests. Follow these conventions unless a maintainer requests an exception.
-
Config precedence and new options
- Global precedence:
options.yml
overrides CLI flags and environment variables, if present. Don’t special‑case a single option. - Adding a new option:
- Add a field to
internal/config/options.go
withyaml:"…"
and aflag:"…"
tag. - Register a CLI flag and env mapping in
internal/config/flags.go
(useEnvVars(...)
). - Expose a getter on
*config.Config
in the relevant file (e.g., cluster options inconfig_cluster.go
). - Add name/value to
rows
in*config.Report()
, after the same option as ininternal/config/options.go
forphotoprism show config
to report it (obfuscate passwords with*
). - If the value must persist (e.g., a generated UUID), write it back to
options.yml
using a focused helper that merges keys. - Tests: cover CLI/env/file precedence and persistence. When tests need a new flag, add it to
CliTestContext
ininternal/config/test.go
.
- Add a field to
- Example:
PortalUUID
precedence =options.yml
→ CLI/env (--portal-uuid
/PHOTOPRISM_PORTAL_UUID
) → generate UUIDv4 and persist. - CLI flag precedence: when you need to favor an explicit CLI flag over defaults, check
c.cliCtx.IsSet("<flag>")
before applying additional precedence logic. - Persisting generated options: when writing to
options.yml
, setc.options.OptionsYaml = filepath.Join(c.ConfigPath(), "options.yml")
and reload the file to keep in‑memory
- Global precedence:
-
Database access
- The app uses GORM v1. Don’t use
WithContext
; for executing raw SQL, preferdb.Raw(stmt).Scan(&nop)
. - When provisioning MariaDB/MySQL objects, quote identifiers with backticks and limit the character set; avoid building identifiers from untrusted input.
- Reuse
conf.Db()
andconf.Database*()
getters; reject unsupported drivers early with a clear error.
- The app uses GORM v1. Don’t use
-
Rate limiting
- Reuse the existing limiter in
internal/server/limiter
(e.g.,limiter.Auth
/limiter.Login
). - For 429s, use
limiter.AbortJSON(c)
when applicable; avoid creating new limiter stacks.
- Reuse the existing limiter in
-
API handlers
- Use existing helpers:
api.ClientIP(c)
,header.BearerToken(c)
,Abort*
functions for errors. - Compare secrets/tokens using constant‑time compare; don’t log secrets.
- Set
Cache-Control: no-store
on responses containing secrets. - Register new routes in
internal/server/routes.go
. Don’t editswagger.json
directly—runmake swag
to regenerate. - Portal mode: set
PHOTOPRISM_NODE_TYPE=portal
andPHOTOPRISM_PORTAL_TOKEN
. - Pagination defaults: for new list endpoints, prefer
count
default 100 (max 1000) andoffset
≥ 0; document both in Swagger and validate bounds in handlers. - Document parameters explicitly in Swagger annotations (path, query, and body) so
make swag
produces accurate docs. - Swagger:
make fmt-go swag-fmt && make swag
after adding or changing API annotations. - Focused tests:
go test ./internal/api -run Cluster -count=1
(or limit to the package you changed).
- Use existing helpers:
-
Registry & secrets
- Store portal/node registry data under
conf.PortalConfigPath()/nodes/
as YAML with file mode0600
. - Keep node secrets out of logs and omit them from JSON responses unless explicitly returned on creation/rotation.
- Store portal/node registry data under
-
Testing patterns
- Use
t.TempDir()
for isolated config paths and files. After changingConfigPath
post‑construction, reloadoptions.yml
intoc.options
if needed. - Prefer small, focused unit tests; use existing test helpers (
NewConfig
,CliTestContext
, etc.). - API tests: use
NewApiTest()
,PerformRequest*
,AuthenticateAdmin
/AuthenticateUser
, andOAuthToken
for client-scope scenarios. - Permissions: cover public=false (401), CDN headers (403), admin access (200), and client tokens with insufficient scope (403).
- Auth mode in tests: use
conf.SetAuthMode(config.AuthModePasswd)
(and defer restore) instead of flippingOptions().Public
; this toggles related internals used by tests. - Fixtures caveat: user fixtures often have admin role; for negative permission tests, prefer OAuth client tokens with limited scope rather than relying on a non‑admin user.
- Use
-
Known tooling constraints
- Python may not be available in the dev container; prefer
apply_patch
, Go, or Make targets over ad‑hoc scripts. make swag
may fetch modules; ensure network availability in CI before running.
- Python may not be available in the dev container; prefer
Cluster & Bootstrap Quick Tips
-
Import rules (avoid cycles):
- Do not import
internal/service/cluster/instance/*
frominternal/config
or the cluster root package. - Instance/service bootstraps talk to the Portal via HTTP(S); do not import Portal internals such as
internal/api
orinternal/service/cluster/registry
/provisioner
. - Prefer constants from
internal/service/cluster/const.go
(e.g.,cluster.Instance
,cluster.Portal
) over string literals.
- Do not import
-
Early extension lifecycle (config.Init sequence):
- Load
options.yml
and settings (c.initSettings()
) - Run early hooks:
EarlyExt().InitEarly(c)
(may adjust DB settings) - Connect DB:
connectDb()
andRegisterDb()
- Run regular extensions:
Ext().Init(c)
- Load
-
Theme endpoint usage:
- Server:
GET /api/v1/cluster/theme
generates a zip fromconf.ThemePath()
; returns 200 with a valid (possibly empty) zip or 404 if missing. - Client/CLI: install only if
ThemePath()
is missing or lacksapp.js
; do not overwrite unless explicitly allowed. - Use header helpers/constants from
pkg/service/http/header
(e.g.,header.Accept
,header.ContentTypeZip
,header.SetAuthorization
).
- Server:
-
Registration (instance bootstrap):
- Send
rotate=true
only if driver is MySQL/MariaDB and no DSN/name/user/password is configured (including *_FILE for password); never for SQLite. - Treat 401/403/404 as terminal; apply bounded retries with delay for transient/network/429.
- Persist only missing
NodeSecret
and DB settings when rotation was requested.
- Send
-
Testing patterns:
- Set
PHOTOPRISM_STORAGE_PATH=$(mktemp -d)
(ort.Setenv
) to isolate options.yml and theme dirs. - Use
httptest
for Portal endpoints andpkg/fs.Unzip
with size caps for extraction tests.
- Set