π¨βπ» Frontend
The frontend is a React 18 single-page app, written in TypeScript and built with Vite. Mantine provides the UI components, TanStack Router handles routing, and TanStack Query handles server state.
The full tech stack, the development commands (yarn start, yarn test, yarn build) and the code
style conventions are documented in
apps/frontend/README.md,
which lives next to the code. Please read that first β this page only covers what it does not:
debugging, and how the source tree is laid out.
β¨ Code Standardsβ
We use ESLint and Prettier to keep our code tidy. Before you commit, run yarn lint:error to check
your changes and yarn lint:error:fix to apply the automatic fixes β the ESLint config enforces
Prettier through the prettier/prettier rule, so linting also flags unformatted code. The
lint-frontend CI workflow runs the lint, test and build steps on every pull request that touches
apps/frontend and fails on anything unclean, so nothing un-linted gets merged.
yarn build does not type-check, so run yarn typecheck as well. It runs tsc --noEmit over the app
and vite.config.ts. The frontend has zero type errors and no baseline: any error fails it, and so
does the typecheck-frontend CI workflow. Fix the type rather than working around it.
The repository ships a husky pre-commit hook (apps/frontend/.husky/pre-commit, which runs
lint-staged), but it is currently inactive in the monorepo: yarn install runs the prepare
script β husky β with the working directory set to apps/frontend, which has no .git of
its own, so husky exits without registering the hook. Don't rely on it; lint before you commit.
π§ͺ End-to-end testsβ
apps/frontend/e2e holds a Playwright smoke suite: log in, the timeline
shows photos, the lightbox opens, a favorite toggles and persists, log out. It has its own
package.json, separate from the app's. The e2e workflow runs it nightly, on demand, and on pull
requests labelled e2e, and uploads the Playwright report, traces and container logs when it fails.
Against the Docker e2e stack (what CI does). deploy/compose/docker-compose.e2e.yml builds the
proxy, frontend and backend from your checkout, creates admin/admin and scans the eight sample
photos in deploy/e2e/photos:
docker compose -f deploy/compose/docker-compose.e2e.yml up -d --build --wait
cd apps/frontend/e2e
yarn install
yarn playwright install chromium
yarn test # or `yarn test:ui` for the interactive runner
docker compose -f ../../../deploy/compose/docker-compose.e2e.yml down -v
Against a native stack (no Docker, see
Native Windows Setup). Use a fresh
data directory and an admin/admin account, copy deploy/e2e/photos to <DataDir>\data\e2e (it
has to be inside the backend's data root), then point the suite at the Vite dev server and that folder:
$env:E2E_BASE_URL = "http://localhost:3000"
$env:E2E_SCAN_DIR = "C:\librephotos-devdata\data\e2e"
yarn test
The setup step sets the admin's scan directory and starts a scan when the account has no photos yet.
A scan started this way first downloads the ML models (about 1.3 GB, once). The specs expect exactly
the sample library, so run them against an account with no other photos. E2E_USERNAME and
E2E_PASSWORD override the credentials.
π Debuggingβ
React Debug Toolβ
React provides a debug tool, which you can download here.
WDYRβ
WDYR explains to you why a component re-rendered, by logging the changed props and hooks to the browser console. It is wired up in src/wdyr.ts and only runs in development builds.
Set VITE_APP_WDYR to the literal string true β any other value (True, 1, unset) leaves it
off. Where you set it depends on how you run the frontend:
-
Local dev server (
yarn start) β addVITE_APP_WDYR=truetoapps/frontend/.env.development. -
Docker dev stack β add
VITE_APP_WDYR=truetodeploy/compose/.env, then recreate the container so it picks up the new environment:docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d frontend
Either way the dev server has to be restarted. WDYR needs its own JSX transform, which
vite.config.ts only selects when the variable is set at startup, so flipping it mid-session has no
effect.
REST APIβ
Our REST API is documented with Swagger and ReDoc. Once your development stack is up, both are served from the running instance β see Development Installation for the URLs.
ποΈ Structureβ
Everything below is relative to apps/frontend/src.
- routes holds the
pages. Routing is file-based: the path of a file determines its URL. Routes under
_protectedrequire a logged-in user, routes underpublicdo not, and__root.tsxis the shell they all render into.routeTree.gen.tsis generated from this folder by the TanStack Router plugin β never edit it by hand. - Pages should be split up into React components, which you can find in components, grouped by feature.
- api_client
is the web side of the API layer, and every request starts there. The requests themselves live in
packages/api-client,
which the mobile app uses too: one endpoint function per call in
src/endpoints, each parsing the answer with its zod schema fromsrc/schemasand returning the parsed type.api.tssets upapiClient, the shared client with the web's policy around it: it attaches the access token, refreshes it on a 401, logs out when that fails, rejects a failed request withApiError, and shows a "please report this" notification when an answer does not match its schema. Around it, one folder per domain (photos,albums,faces, β¦) holds the TanStack Query hooks the components actually call (useFetchβ¦Query,useβ¦Mutation): each wraps anendpoints.<name>(apiClient, β¦)call and adds the web's cache invalidation and notifications, with keys from the domain's key factory inpackages/api-client/src/hooks/queryKeys.ts, which the mobile app's hooks share. Files under/media/(thumbnails, originals, videos) get their URLs frommediaUrland are fetched withfetchMedia, both inapi_client/media/mediaUrl.ts. A component never callsapiClientorfetchitself: when it needs a request, add the endpoint and its schema (with a fixture test) topackages/api-client, then a hook to the domain's folder. Lint enforces this: no file inapi_clientcallsapiClientdirectly or defines its own zod schema (#2347). - hooks and service contain cross-cutting hooks and helpers, such as authentication and notifications.
- locales holds the
translations, set up in
i18n.ts. Translations are contributed through Weblate rather than edited directly: itslibrephotos-frontendcomponent translatesapps/frontend/src/locales/*/translation.jsonagainst the English file,apps/frontend/src/locales/en/translation.json. English ships in the main bundle, andi18n.tsloads every other directory underlocalesas its own chunk, so a language Weblate adds loads without a code change. To offer it in the Profile language picker, give it an entry incomponents/settings/languageOptions.tsand a dayjs locale indayjsLocaleLoaders(i18n.ts);src/i18n.test.tsfails until both are there. - In util you can find miscellaneous functions.