# Library Visibility (family-lens rule) Every endpoint that returns media metadata or content must respect per-user **library visibility**: the `library_visibility` table maps `(user_id, library_id, is_visible)` and defaults to **ALLOW** — every user sees every library unless an admin explicitly hides one from them. The point of the system is the family case: an admin hides the adult library from a child's account, and no trace of that library (titles, covers, series, page images) may reach the child's account. ## The rule for new endpoints Any endpoint that lists, filters, or details media/series/books: 1. Filter by the requesting user's visible-library set. 2. Reject an explicit `library_id` outside that set with **403** — never silently ignore or fall back. 3. Content endpoints (file bytes, page images) gate per item, like `ReaderHandler.ensureItemVisible`. If you forget, hidden libraries leak. This exact leak shipped: the series endpoints were name-only across ALL libraries and the PDF thumbnail endpoint served page images of hidden books (fixed 2026-10-13, bookhoard `431fec0`). ## Helpers — do not hand-roll - `(*MediaHandler).visibleLibrarySet` / `(*SeriesHandler).visibleLibrarySet` — returns a membership map plus the id slice (map for param checks, slice for SQL `ANY()`). - `router.visibleLibraryIDsFor` — same for SSR routes. - `resolveLibrary` (router/helpers.go) only honors a requested `library_id` that is inside the caller's visible set. - SQL pattern (see `ListMediaItems`, `GetDistinctSeries`, `SearchMediaItems`): ```sql AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR library_id = ANY(sqlc.narg('visible_library_ids')::uuid[])) ``` ## Known-safe surfaces - OPDS device catalogs filter by the device owner's visible libraries. - Search (`SearchMediaItems`) is visibility-scoped. - File download, reader page render, and page content gate per item. ## Series are per-library Series are grouped by `(library_id, series)` — the same series name in two libraries is two entries (manga adaptations, LN splits). Do not re-merge them cross-library. `/api/series/books` REQUIRES a visible `library_id`; `/api/series` without one aggregates only the caller's visible libraries.