CodeVetter · Repo Unpack · Contributor handoff
immich-app/immich
This is a large, convention-heavy monorepo — change one layer and the generated artifacts (OpenAPI spec, SDK, migrations, mobile client) must be regenerated and kept in sync or CI fails. Server code follows strict layering (controller → service → repository → Kysely) with decorator-driven registration that will fail loudly at startup if misconfigured. Tests are fast to run per-package but e2e/medium tiers need Docker. Read the bootstrap and decorator machinery before editing anything structural.
- Read first (server): server/src/main.ts (supervisor), server/src/app.module.ts (module wiring), server/src/enum.ts (all JobName/QueueName/ImmichWorker/Permission values), server/src/decorators.ts (@OnJob/@OnEvent/@Endpoint/@Chunked), server/src/services/base.service.ts (repository injection convention).
- Convention: controllers stay thin — they take @Auth() AuthDto + DTOs and delegate to a service; every handler must declare @Authenticated() (defaults to authenticated; use {public:true} or {sharedLink:true} variants) or the guard throws.
- Convention: authorization inside services goes through this.requireAccess({auth, permission, ids}) / checkAccess backed by access.repository.ts — don't hand-roll ownership checks.
- Adding a job is a startup-validated contract: new JobName without an @OnJob handler (or a duplicate handler) aborts boot with ImmichStartupError — always add enum + decorated method + ensure the service is in services/index.ts.
- API changes must be followed by regeneration: sync-open-api.ts writes the spec, 'mise run open-api-typescript' rebuilds packages/sdk, and the check-openapi CI workflow fails if the committed spec drifts.
- Schema changes: edit table classes under server/src/schema/tables, then 'pnpm migrations:generate' (sql-tools against DB_URL); ordering is enforced by schema/migrations/ORDER and the migration-order.yml workflow — see docs/docs/developer/database-migrations.md.
- Test commands: server unit 'pnpm test' (vitest, no DB needed), 'pnpm test:medium' (needs Docker for testcontainers), e2e 'pnpm test'/'test:web' in e2e/ (needs docker compose up), web 'pnpm test'. Lint gates are strict: eslint --max-warnings 0 plus prettier checks.
- Known trap: services get repositories positionally through BaseService — adding a repository means updating repositories/index.ts AND the BASE_SERVICE_DEPENDENCIES tuple AND the constructor AND BaseService.create in the same order, or DI silently misaligns.
- Known trap: file paths on disk must go through StorageCore/StorageRepository helpers (StorageFolder layout, media location from IMMICH_MEDIA_LOCATION) — never construct storage paths ad hoc, and DB-stored paths are updated via MoveRepository.
- Known trap: long-running requests are normal (24h requestTimeout, streaming uploads/downloads via sendFile/ImmichFileResponse) — avoid buffering whole files in memory and prefer the ImmichFileResponse pattern for binary responses.
- Environment config is centralized: add new env vars to EnvSchema in dtos/env.dto.ts and surface them via ConfigRepository.getEnv(); never read process.env inside services.
- Toolchain is pinned by mise (node 24.21.0, pnpm 11.24.0, java 21 for openapi-generator, extism/binaryen for plugins, jellyfin-ffmpeg for transcoding tests) — use 'mise run //server:ci-unit' style tasks to match CI rather than raw pnpm where possible.