Two small documentation gaps the second-pass audit flagged:
- CLAUDE.md described only the Vite dev proxy ("Vite dev-proxies to
the backend"), which left the production path opaque. Now lists
both: the Vite proxy for `npm run dev` and
`frontend/src/hooks.server.ts` for adapter-node. Same-origin cookie
story called out explicitly.
- `/api/v1/files/{key}` is an unauthenticated capability URL by
design — reads stay public, keys are unguessable v4 UUIDs, leaked
URL leaks one file. Documented both in `backend/src/api/files.rs`'s
module doc (with a pointer at the seam a future
feat/private-libraries branch would use) and in a new "Capability
URLs" section in README so a casual reader doesn't mistake the lack
of auth for an oversight.
No code or behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
143 lines
8.9 KiB
Markdown
143 lines
8.9 KiB
Markdown
# 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/<slug>`, `bugfix/<slug>`, `chore/<slug>`, `docs/<slug>`, `refactor/<slug>`, `test/<slug>`. 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_<name>.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/<resource>.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`. Two layers route `/api/*` to axum depending on environment:
|
|
- **`npm run dev`** uses the Vite proxy in [vite.config.ts](frontend/vite.config.ts) to forward `/api` to `http://localhost:8080`.
|
|
- **Production / adapter-node** uses [frontend/src/hooks.server.ts](frontend/src/hooks.server.ts) to reverse-proxy `/api/*` to `BACKEND_URL` (compose wires `http://backend:8080`). The browser only ever talks to the SvelteKit container on `:3000`, so cookies stay same-origin and `CORS_ALLOWED_ORIGINS` can stay empty.
|
|
- [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).
|