26 KiB
PhotoPrism® Repository Guidelines
Last Updated: September 25, 2025
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)
- Code Maps:
CODEMAP.md
(Backend/Go),frontend/CODEMAP.md
(Frontend/JS)
Specifications (Versioning & Usage)
- Always use the latest spec version for a topic (highest
-vN
), as linked fromspecs/README.md
and the portal cheatsheet (specs/portal/README.md
). - Testing Guides:
specs/dev/backend-testing.md
(Backend/Go),specs/dev/frontend-testing.md
(Frontend/JS) - Whenever the Change Management instructions for a document require it, publish changes as a new file with an incremented version suffix (e.g.,
*-v3.md
) rather than overwriting the original file. - Older spec versions remain in the repo for historical reference but are not linked from the main TOC. Do not base new work on superseded files (e.g.,
*-v1.md
when*-v2.md
exists).
Note on specs repository availability
- The
specs/
repository may be private and is not guaranteed to be present in every clone or environment. Do not add Makefile targets in the main project that depend onspecs/
paths. Whenspecs/
is available, run its tools directly (e.g.,bash specs/scripts/lint-status.sh
).
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:
- 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). - 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
FFmpeg Tests & Hardware Gating
- By default, do not run GPU/HW encoder integrations in CI. Gate with
PHOTOPRISM_FFMPEG_ENCODER
(one of:vaapi
,intel
,nvidia
). - Negative-path tests should remain fast and always run:
- Missing ffmpeg binary → immediate exec error.
- Unwritable destination → command fails without creating files.
- Prefer command-string assertions when hardware is unavailable; enable HW runs locally only when a device is configured.
Fast, Focused 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
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.
- Every Go package must contain a
<package>.go
file in its root (for example,internal/auth/jwt/jwt.go
) with the standard license header and a short package description comment explaining its purpose. - 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.
Filesystem Permissions & io/fs Aliasing (Go)
- Always use our shared permission variables from
pkg/fs
when creating files/directories:- Directories:
fs.ModeDir
(0o755 with umask) - Regular files:
fs.ModeFile
(0o644 with umask) - Config files:
fs.ModeConfigFile
(default 0o664) - Secrets/tokens:
fs.ModeSecretFile
(default 0o600) - Backups:
fs.ModeBackupFile
(default 0o600)
- Directories:
- Do not pass stdlib
io/fs
flags (e.g.,fs.ModeDir
) to functions expecting permission bits.- When importing the stdlib package, alias it to avoid collisions:
iofs "io/fs"
orgofs "io/fs"
. - Our package is
github.com/photoprism/photoprism/pkg/fs
and provides the only approved permission constants foros.MkdirAll
,os.WriteFile
,os.OpenFile
, andos.Chmod
.
- When importing the stdlib package, alias it to avoid collisions:
- Prefer
filepath.Join
for filesystem paths; reservepath.Join
for URL paths.
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.
-
File I/O — Overwrite Policy (force semantics)
-
Default is safety-first: callers must not overwrite non-empty destination files unless they opt-in with a
force
flag. -
Replacing empty destination files is allowed without
force=true
(useful for placeholder files). -
Open destinations with
O_WRONLY|O_CREATE|O_TRUNC
to avoid trailing bytes when overwriting; useO_EXCL
when the caller must detect collisions. -
Where this lives:
- App-level helpers:
internal/photoprism/mediafile.go
(MediaFile.Copy/Move
). - Reusable utils:
pkg/fs/copy.go
,pkg/fs/move.go
.
- App-level helpers:
-
When to set
force=true
:- Explicit “replace” actions or admin tools where the user confirmed overwrite.
- Not for import/index flows; Originals must not be clobbered.
-
Archive Extraction — Security Checklist
-
Always validate ZIP entry names with a safe join; reject:
- absolute paths (e.g.,
/etc/passwd
). - Windows drive/volume paths (e.g.,
C:\\…
orC:/…
). - any entry that escapes the target directory after cleaning (path traversal via
..
).
- absolute paths (e.g.,
-
Enforce per-file and total size budgets to prevent resource exhaustion.
-
Skip OS metadata directories (e.g.,
__MACOSX
) and reject suspicious names. -
Where this lives:
pkg/fs/zip.go
(Unzip
,UnzipFile
,safeJoin
). -
Tests to keep:
- Absolute/volume paths rejected (Windows-specific backslash path covered on Windows).
..
traversal skipped;__MACOSX
skipped.- Per-file and total size limits enforced; directory entries created; nested paths extracted safely.
-
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
HTTP Download — Security Checklist
- Use the shared safe HTTP helper instead of ad‑hoc
net/http
code:- Package:
pkg/service/http/safe
→safe.Download(destPath, url, *safe.Options)
. - Default policy in this repo: allow only
http/https
, enforce timeouts and max size, write to a0600
temp file then rename.
- Package:
- SSRF protection (mandatory unless explicitly needed for tests):
- Set
AllowPrivate=false
to block private/loopback/multicast/link‑local ranges. - All redirect targets are validated; the final connected peer IP is also checked.
- Prefer an image‑focused
Accept
header for image downloads:"image/jpeg, image/png, */*;q=0.1"
.
- Set
- Avatars and small images: use the thin wrapper in
internal/thumb/avatar.SafeDownload
which applies stricter defaults (15s timeout, 10 MiB,AllowPrivate=false
). - Tests using
httptest.Server
on 127.0.0.1 must passAllowPrivate=true
explicitly to succeed. - Keep per‑resource size budgets small; rely on
io.LimitReader
+Content-Length
prechecks.
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 Quick Tips (Do This)
Next‑Session Priorities
- If we add Postgres provisioning support, extend
BuildDSN
andprovisioner.DatabaseDriver
handling, add validations, and returndriver=postgres
consistently in API/CLI. - Consider surfacing a short “uuid → db/user” mapping helper in the CLI (e.g.,
nodes show --creds
) if operators request it.
Testing & Fixtures
- Go tests live next to their sources (
path/to/pkg/<file>_test.go
); group related cases ast.Run(...)
sub-tests to keep table-driven coverage readable. - Prefer focused
go test
runs for speed (go test ./internal/<pkg> -run <Name> -count=1
,go test ./internal/commands -run <Name> -count=1
) and avoid./...
unless you need the entire suite. - Heavy packages such as
internal/entity
andinternal/photoprism
run migrations and fixtures; expect 30–120s on first run and narrow with-run
to keep iterations low. - For CLI-driven tests, wrap commands with
RunWithTestContext(cmd, args)
sourfave/cli
cannot exit the process, and assert CLI output withassert.Contains
/regex becauseshow
reports quote strings. - In
internal/photoprism
tests, rely onphotoprism.Config()
for runtime-accurate behavior; only build a new config if you replace it viaphotoprism.SetConfig
. - Generate identifiers with
rnd.GenerateUID(entity.ClientUID)
for OAuth client IDs andrnd.UUIDv7()
for node UUIDs; treatnode.uuid
as required in responses. - Shared fixtures live under
storage/testdata
;NewTestConfig("<pkg>")
already callsInitializeTestData()
, but callc.InitializeTestData()
(and optionallyc.AssertTestData(t)
) when you construct custom configs so originals/import/cache/temp exist.InitializeTestData()
clears old data, downloads fixtures if needed, then callsCreateDirectories()
.
Roles & ACL
- Map roles via the shared tables: users through
acl.ParseRole(s)
/acl.UserRoles[...]
, clients throughacl.ClientRoles[...]
. - Treat
RoleAliasNone
("none") and an empty string asRoleNone
; no caller-specific overrides. - Default unknown client roles to
RoleClient
;acl.ParseRole
already handles0/false/nil
as none for users. - Build CLI role help from
Roles.CliUsageString()
(e.g.,acl.ClientRoles.CliUsageString()
); never hand-maintain role lists.
Import/Index
- ImportWorker may skip files if an identical file already exists (duplicate detection). Use unique copies or assert DB rows after ensuring a non‑duplicate destination.
- Mixed roots: when testing related files, keep
ExamplesPath()/ImportPath()/OriginalsPath()
consistent soRelatedFiles
andAllowExt
behave as expected.
CLI Usage & Assertions
- Wrap CLI tests in
RunWithTestContext(cmd, args)
sourfave/cli
cannot exit the process; assert quotedshow
output withassert.Contains
/regex for the trailing ", or " rule. - Prefer
--json
responses for automation.photoprism show commands --json [--nested]
exposes the tree view (add--all
for hidden entries). - Use
internal/commands/catalog
to inspect commands/flags without running the binary; when validating large JSON docs, marshal DTOs viacatalog.BuildFlat/BuildNode
instead of parsing CLI stdout. - Expect
show
commands to return arrays of snake_case rows, exceptphotoprism show config
, which yields{ sections: [...] }
, and theconfig-options
/config-yaml
variants, which flatten to a top-level array.
API & Config Changes
- Respect precedence:
options.yml
overrides CLI/env values, which override defaults. When adding a new option, updateinternal/config/options.go
(yaml/flag tags), register it ininternal/config/flags.go
, expose a getter, surface it in*config.Report()
, and write generated values back tooptions.yml
by settingc.options.OptionsYaml
before persisting. UseCliTestContext
ininternal/config/test.go
to exercise new flags. - When touching configuration in Go code, use the public accessors on
*config.Config
(e.g.Config.JWKSUrl()
,Config.SetJWKSUrl()
,Config.ClusterUUID()
) instead of mutatingConfig.Options()
directly; reserve raw option tweaks for test fixtures only. - Logging: use the shared logger (
event.Log
) via the package-levellog
variable (seeinternal/auth/jwt/logger.go
) instead of directfmt.Print*
or ad-hoc loggers. - Cluster registry tests (
internal/service/cluster/registry
) currently rely on a full test config because they persistentity.Client
rows. They run migrations and seed the SQLite DB, so they are intentionally slow. If you refactor them, consider sharing a singleconfig.TestConfig()
across subtests or building a lightweight schema harness; do not swap to the minimal config helper unless the tests stop touching the database. - Favor explicit CLI flags: check
c.cliCtx.IsSet("<flag>")
before overriding user-supplied values, and follow theClusterUUID
pattern (options.yml
→ CLI/env → generated UUIDv4 persisted). - Database helpers: reuse
conf.Db()
/conf.Database*()
, avoid GORMWithContext
, quote MySQL identifiers, and reject unsupported drivers early. - Handler conventions: reuse limiter stacks (
limiter.Auth
,limiter.Login
) andlimiter.AbortJSON
for 429s, lean onapi.ClientIP
,header.BearerToken
, andAbort*
helpers, compare secrets with constant time checks, setCache-Control: no-store
on sensitive responses, and register routes ininternal/server/routes.go
. For new list endpoints defaultcount=100
(max 1000) andoffset≥0
, document parameters explicitly, and set portal mode viaPHOTOPRISM_NODE_ROLE=portal
plusPHOTOPRISM_JOIN_TOKEN
when needed. - Swagger & docs: annotate only routed handlers in
internal/api/*.go
, use full/api/v1/...
paths, skip helpers, and regenerate docs withmake fmt-go swag-fmt swag
ormake swag-json
(which also strips duplicatetime.Duration
enums). When iterating, target packages withgo test ./internal/api -run Cluster -count=1
or similarly scoped runs. - Testing helpers: isolate config paths with
t.TempDir()
, reuseNewConfig
,CliTestContext
, andNewApiTest()
harnesses, authenticate viaAuthenticateAdmin
,AuthenticateUser
, orOAuthToken
, toggle auth withconf.SetAuthMode(config.AuthModePasswd)
, and prefer OAuth client tokens over non-admin fixtures for negative permission checks. - Registry data and secrets: store portal/node registry files under
conf.PortalConfigPath()/nodes/
with mode0600
, keep secrets out of logs, and only return them on creation/rotation flows.
Formatting (Go)
- Go is formatted by
gofmt
and uses tabs. Do not hand-format indentation. - Always run after edits:
make fmt-go
(gofmt + goimports).
API Shape Checklist
- When renaming or adding fields:
- Update DTOs in
internal/service/cluster/response.go
and any mappers. - Update handlers and regenerate Swagger:
make fmt-go swag-fmt swag
. - Update tests (search/replace old field names) and examples in
specs/
. - Quick grep:
rg -n 'oldField|newField' -S
across code, tests, and specs.
- Update DTOs in
API/CLI Tests: Known Pitfalls
- Gin routes: Register
CreateSession(router)
once per test router; reusing it twice panics on duplicate route. - CLI commands: Some commands defer
conf.Shutdown()
or emit signals that close the DB. The harness re‑opens DB before each run, but avoid invokingstart
or emitting signals in unit tests. - Signals:
internal/commands/start.go
waits onprocess.Signal
; callingprocess.Shutdown()/Restart()
can close DB. Prefer not to trigger signals in tests.
Download CLI Workbench (yt-dlp, remux, importer)
-
Code anchors
- CLI flags and examples:
internal/commands/download.go
- Core implementation (testable):
internal/commands/download_impl.go
- yt-dlp helpers and arg wiring:
internal/photoprism/dl/*
(options.go
,info.go
,file.go
,meta.go
) - Importer entry point:
internal/photoprism/get/import.go
; options:internal/photoprism/import_options.go
- CLI flags and examples:
-
Quick test runs (fast feedback)
- yt-dlp package:
go test ./internal/photoprism/dl -run 'Options|Created|PostprocessorArgs' -count=1
- CLI command:
go test ./internal/commands -run 'DownloadImpl|HelpFlags' -count=1
- yt-dlp package:
-
FFmpeg-less tests
- In tests: set
c.Options().FFmpegBin = "/bin/false"
andc.Settings().Index.Convert = false
to avoid ffmpeg dependencies when not validating remux.
- In tests: set
-
Stubbing yt-dlp (no network)
- Use a tiny shell script that:
- prints minimal JSON for
--dump-single-json
- creates a file and prints its path when
--print
is requested
- prints minimal JSON for
- Harness env vars (supported by our tests):
YTDLP_ARGS_LOG
— append final args for assertionYTDLP_OUTPUT_FILE
— absolute file path to create for--print
YTDLP_DUMMY_CONTENT
— file contents to avoid importer duplicate detection between tests
- Use a tiny shell script that:
-
Remux policy and metadata
- Pipe method: PhotoPrism remux (ffmpeg) always embeds title/description/created.
- File method: yt‑dlp writes files; we pass
--postprocessor-args 'ffmpeg:-metadata creation_time=<RFC3339>'
so imports getCreated
even without local remux (fallback fromupload_date
/release_date
). - Default remux policy:
auto
; usealways
for the most complete metadata (chapters, extended tags).
-
Testing workflow: lean on the focused commands above; if importer dedupe kicks in, vary bytes with
YTDLP_DUMMY_CONTENT
or adjustdest
, and rememberinternal/photoprism
is heavy so validate downstream packages first.
Sessions & Redaction (building sessions in tests)
- Admin session (full view):
AuthenticateAdmin(app, router)
. - User session: Create a non‑admin test user (role=guest), set a password, then
AuthenticateUser
. - Client session (redacted internal fields;
siteUrl
visible):Admins sees, _ := entity.AddClientSession("test-client", conf.SessionMaxAge(), "cluster", authn.GrantClientCredentials, nil) token := s.AuthToken() r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster/nodes", token)
advertiseUrl
anddatabase
; client/user sessions don’t.siteUrl
is safe to show to all roles.
Preflight Checklist
go build ./...
make fmt-go swag-fmt swag
go test ./internal/service/cluster/registry -count=1
go test ./internal/api -run 'Cluster' -count=1
go test ./internal/commands -run 'ClusterRegister|ClusterNodesRotate' -count=1
- Tooling constraints:
make swag
may fetch modules, so confirm network access before running it.
Cluster Operations
- Keep bootstrap code decoupled: avoid importing
internal/service/cluster/instance/*
frominternal/config
or the cluster root, let instances talk to the Portal over HTTP(S), and rely on constants frominternal/service/cluster/const.go
. - Config init order: load
options.yml
(c.initSettings()
), runEarlyExt().InitEarly(c)
, connect/register the DB, then invokeExt().Init(c)
. - Theme endpoint:
GET /api/v1/cluster/theme
streams a zip fromconf.ThemePath()
; only reinstall whenapp.js
is missing and always use the header helpers inpkg/service/http/header
. - Registration flow: send
rotate=true
only for MySQL/MariaDB nodes without credentials, treat 401/403/404 as terminal, includeclientId
+clientSecret
when renaming an existing node, and persist only newly generated secrets or DB settings. - Registry & DTOs: use the client-backed registry (
NewClientRegistryWithConfig
)—the file-backed version is legacy—and treat migration as complete only after swapping callsites, building, and running focused API/CLI tests. Nodes are keyed by UUID v7 (/api/v1/cluster/nodes/{uuid}
), the registry interface stays UUID-first (Get
,FindByNodeUUID
,FindByClientID
,RotateSecret
,DeleteAllByUUID
), CLI lookups resolveuuid → clientId → name
, and DTOs normalizedatabase.{name,user,driver,rotatedAt}
while exposingclientSecret
only during creation/rotation.nodes rm --all-ids
cleans duplicate client rows, admin responses may includeadvertiseUrl
/database
, client/user sessions stay redacted, registry files live underconf.PortalConfigPath()/nodes/
(mode 0600), andClientData
no longer storesNodeUUID
. - Provisioner & DSN: database/user names use UUID-based HMACs (
photoprism_d<hmac11>
,photoprism_u<hmac11>
);BuildDSN
accepts adriver
but falls back to MySQL format with a warning when unsupported. - Testing: exercise Portal endpoints with
httptest
, guard extraction paths withpkg/fs.Unzip
size caps, and expect admin-only fields to disappear when authenticated as a client/user session.