# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Mangalord is a manga / comics reader. Users browse, search, read, bookmark, and upload manga. The same HTTP API is consumed by both the SvelteKit frontend and external bots/scripts — there is no separate "bot API" or "internal API." Deployment is a single server via Docker Compose. ## Stack - **Backend**: Rust, axum 0.7, sqlx 0.8 (Postgres), tokio - **Database**: Postgres 16 - **Frontend**: SvelteKit 2 with Svelte 5 runes, TypeScript, Vite - **Storage**: `Storage` trait + `LocalStorage` impl. S3 and friends are planned implementations, not refactors. - **Tests**: `cargo test` (unit + integration via `#[sqlx::test]`), Vitest (frontend unit), Playwright (E2E) ## Development workflow rules These rules apply to every change. They are not negotiable per-task — if a change can't be made under them, raise the conflict before writing code. ### TDD Tests describing the expected behaviour are written **before** the implementation, in the same commit or one immediately preceding. A commit that adds production code without a corresponding test (or that updates a test only after the fact to match what was built) does not meet this bar — split it. See *TDD workflow* below for picking the right test level. ### Git - One branch per logical change, prefixed by intent: `feat/`, `bugfix/`, `chore/`, `docs/`, `refactor/`, `test/`. Never commit directly to `main`. - Commit messages follow Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`). One concern per commit. - Each branch lands via a reviewed PR (self-review counts when working solo; the diff must still be read end-to-end before merge). Squash-merge into `main` to keep history linear. - Do not push `main` directly, do not force-push shared branches, and do not bypass hooks. ### Semantic versioning The project version is tracked in [backend/Cargo.toml](backend/Cargo.toml) and [frontend/package.json](frontend/package.json) and bumped on every merge to `main`: - `feat:` → bump **minor** - `fix:` / `bugfix:` → bump **patch** - breaking change (only valid before 1.0, otherwise needs a major bump after 1.0) → bump **minor** while under 1.0 - `chore:` / `docs:` / `test:` / `refactor:` with no behaviour change → no bump While the project is pre-release the version stays **below `1.0.0`**. The `1.0.0` release happens **only at the user's explicit instruction** — do not bump to or past it autonomously. Both manifests must stay in lockstep (same version string). ## TDD workflow The project is developed test-first. For each change: 1. Write a failing test at the right level. 2. Implement the minimum to make it pass. 3. Refactor with tests green. Pick the level deliberately: - **Pure logic / mappers / validation** → unit test next to the code. In Rust: `#[cfg(test)] mod tests` in the same file. In the frontend: a sibling `*.test.ts`. - **Anything that touches the DB** → integration test in [backend/tests/](backend/tests/), using `#[sqlx::test(migrations = "./migrations")]`. Each test gets a fresh, migrated database. - **Cross-component user journeys** → Playwright in [frontend/e2e/](frontend/e2e/). Mock the network at the Playwright route level when the journey doesn't require a real backend. Run these before claiming a change works: ```bash (cd backend && cargo test) (cd frontend && npm test) (cd frontend && npm run test:e2e) # needs dev server, see playwright.config.ts ``` Backend integration tests require `DATABASE_URL` to point at a Postgres where the test user can `CREATEDB` (the `#[sqlx::test]` macro provisions a fresh database per test). ## Backend layout Hexagonal-ish layering. Handlers depend on `repo` and `storage`, never the other way around. This is the seam new extensions plug into. - [backend/src/domain/](backend/src/domain/) — pure data types, no I/O. Add new types here first. - [backend/src/repo/](backend/src/repo/) — DB access functions taking `&PgPool`. Plain async fns rather than a repository struct; tests target them directly. - [backend/src/storage/](backend/src/storage/) — `Storage` trait + `LocalStorage`. **Add `S3Storage` here when needed**, do not branch on backend type in handlers. - [backend/src/api/](backend/src/api/) — axum handlers and route wiring. One module per resource. - [backend/src/app.rs](backend/src/app.rs) — `AppState` and router assembly. Integration tests use the same `router(state)` function with a test `AppState`. - [backend/src/error.rs](backend/src/error.rs) — `AppError` is the single error type returned by handlers. New variants map to status codes in `IntoResponse`. When adding a new resource: 1. Add a migration `backend/migrations/NNNN_.sql`. 2. Add the domain type in [backend/src/domain/](backend/src/domain/). 3. Add repo functions in [backend/src/repo/](backend/src/repo/). 4. Add handlers in `backend/src/api/.rs` and merge in [backend/src/api/mod.rs](backend/src/api/mod.rs). 5. Add integration tests in [backend/tests/](backend/tests/), reusing [backend/tests/common/mod.rs](backend/tests/common/mod.rs). ### Database access Queries use `sqlx::query_as::<_, T>(...)` (runtime-checked) with `#[derive(FromRow)]` on the domain types, so the project builds without a live DB. If you want compile-time SQL checking later, switch to `sqlx::query!`/`query_as!` macros and commit a `.sqlx` directory via `cargo sqlx prepare`. Migrations live in [backend/migrations/](backend/migrations/) and run automatically at startup via `sqlx::migrate!`. ### Storage trait Do not call `tokio::fs` or any filesystem API from handlers. Go through `state.storage` (the `Storage` trait). Keys are `/`-separated paths; the local backend rejects `..` and empty segments. Suggested key layout: - `mangas/{manga_id}/cover.{ext}` - `mangas/{manga_id}/chapters/{chapter_id}/pages/{nnnn}.{ext}` ## Frontend layout - [frontend/src/lib/api/](frontend/src/lib/api/) — typed API client. **All backend calls go through here**, not raw `fetch` in components. The base URL is `import.meta.env.VITE_API_BASE`, defaulting to `/api` (which Vite dev-proxies to the backend). - [frontend/src/routes/](frontend/src/routes/) — SvelteKit routes. - Use Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`). Do not use the legacy `let`-reactive syntax or `on:event=` directive form — prefer `onevent={...}`. - The Node adapter is configured for production; `npm run build && node build` is what the Dockerfile runs. ## Common commands ```bash # Full stack (compose), production-like docker compose up --build # Local dev: Postgres in compose, backend + frontend native docker compose -f docker-compose.dev.yml up -d (cd backend && cargo run) (cd frontend && npm run dev) # Backend tests (cd backend && cargo test) (cd backend && cargo test --test api_mangas) # single integration test file (cd backend && cargo test list_is_empty_initially) # single test by name # Frontend tests (cd frontend && npm test) # vitest, all (cd frontend && npx vitest run src/lib/api/mangas.test.ts) # single file (cd frontend && npm run test:e2e) # playwright (auto-starts vite) ``` ## Extension points These are first-class slots in the architecture. When adding any of them, plug into the existing seam rather than building parallel infrastructure. - **Tags / lists**: new tables joined to `mangas`. New `domain`, `repo`, and `api` modules; the existing manga endpoints do not need to change. - **Full-text / fuzzy search**: enable `pg_trgm` in a migration and add a GIN index on `mangas.title`; swap the `WHERE` in `repo::manga::list` to use `%` operator or `tsvector`. The API shape (`?search=...`) does not change. - **OCR / autotagging**: a background worker (a separate binary or a tokio task spawned in `app::build`) that reads pages from `storage::Storage` and writes tag rows. Do not couple OCR to upload handlers — it runs asynchronously. - **S3 storage**: add `storage::S3Storage` implementing `Storage`. Branch in `app::build` based on a config field (e.g., `STORAGE_BACKEND=s3`). Handlers do not change. - **Auth**: an axum middleware producing a `CurrentUser` extractor. Bots use an API token header, browser users a session cookie — both should populate the same extractor so handlers stay backend-agnostic. Until this lands, treat all endpoints as unauthenticated (this is acknowledged technical debt, not a final design).