- TypeScript 96.5%
- JavaScript 2.2%
- Dockerfile 1%
- HTML 0.3%
| .forgejo/workflows | ||
| .husky | ||
| backend | ||
| e2e | ||
| frontend | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
manga-lib
A personal manga collection tracker. Track which volumes you own, who has read what, manage wishlists, and see what to buy next — for multiple users sharing a single library.
Stack
- Backend: Node.js + Express + Knex — SQLite (
better-sqlite3) for local dev, Postgres in production, selected at runtime by whetherDATABASE_URLis set - Frontend: React 18 + Vite + Tailwind CSS + TanStack Query
- Validation: Zod
- Testing: Vitest (unit) + Playwright (E2E)
Getting Started
Local development
npm run setup # install all deps (root + backend + frontend)
npm run dev # backend on :3001, Vite on :5173
Docker (production)
docker compose up -d
The app is served on port 3000. The SQLite database is persisted in a named volume (manga-data).
compose.yaml includes Traefik labels for running behind a Traefik reverse proxy. This needs the external Docker network Traefik listens on (default traefik, override via TRAEFIK_NETWORK) and TRAEFIK_HOST set to the desired domain.
| Variable | Default | Description |
|---|---|---|
HOST_PORT |
3000 |
Host port the container's port 3000 is mapped to |
TRAEFIK_HOST |
manga-lib.example.com |
Domain Traefik routes to this app |
TRAEFIK_NETWORK |
traefik |
External Docker network Traefik listens on |
TRAEFIK_CERT_RESOLVER |
letsencrypt |
Traefik cert resolver name |
MEILI_HOST |
http://meilisearch:7700 |
Meilisearch instance for library search (see shared-services); unset to fall back to client-side title matching |
MEILI_API_KEY |
— | API key matching the shared Meilisearch instance's MEILI_MASTER_KEY |
Access control (mTLS)
Instead of a password, the router requires a valid client certificate signed by a shared personal CA — see the separate mtls-ca repo for the CA, device-cert issuing script, and the actual TLS Options definition (Docker labels can only reference a tls.options entry with clientAuth settings, not define one, so it's defined via Traefik's file provider and referenced here with the @file suffix).
manga-lib also attaches mtls-ca's pass-client-cert middleware, which forwards the verified cert's CN to the app via the X-Forwarded-Tls-Client-Cert-Info header. This app uses that to map a device's certificate to one of its own user profiles (users.cert_cn): link one with
curl -X PUT https://<domain>/api/users/<id>/cert-cn \
-H "Content-Type: application/json" \
-d '{"cert_cn": "<Name>"}'
where <Name> matches the CN the cert was issued with in mtls-ca (./issue-client-cert.sh "<Name>" — use the person's name, not a device name, so the same cert works from any of their devices). GET /api/me returns the profile the current request's cert is mapped to, or {"user": null} if unmapped.
Once linked, that cert can only act as its own profile for reading progress, shelf status, hidden-manga flags, and creating your own wishlist entries — the API returns 403 if it tries to act as a different profile.
Everything else that isn't scoped to your own profile — managing profiles (add/remove/color/minor flag/cert linking), adding/editing/deleting mangas, the shared buy-list/volumes-owned/borrowed bookkeeping, refreshing original titles, and editing or deleting someone else's wishlist entry — is a household-admin action and requires the linked cert's profile to be an adult (is_minor false); minors get a 403. Editing or deleting your own wishlist entry is always allowed, regardless of age. A request with no linked cert is treated as unrestricted for this check specifically (see the read-only rule above for what that means in production vs. local dev).
In production (NODE_ENV=production, e.g. the Docker image), any request without a cert mapped to a profile — no cert at all, or a cert not yet linked — is read-only: every non-GET/HEAD/OPTIONS request gets a 403, no exceptions. This covers the case where the app's host port is reached directly instead of through Traefik's mTLS gate. Local dev has no Traefik in front at all, so it's exempt from this check.
Scripts
| Command | Description |
|---|---|
npm run setup |
Install all dependencies (root + backend + frontend) |
npm run dev |
Start dev servers (backend :3001, frontend :5173) |
npm run build |
Production build → frontend/dist/ |
npm start |
Start production server on :3000 |
npm run typecheck |
TypeScript check (frontend only) |
npm run lint |
ESLint (frontend) |
npm run format |
Prettier (frontend) |
npm run test |
Vitest unit tests |
npm run test:e2e |
Playwright E2E tests (headless) |
npm run test:e2e:ui |
Playwright E2E tests (interactive UI) |
npm run migrate:make -- <name> |
Create a new Knex migration file in backend/migrations/ |
API
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check ({"status":"ok","database":"connected"}, 503 if the DB ping fails) |
GET |
/api/version |
Git commit hash baked in at build time ({"version":"<hash>"}, "unknown" outside Docker) |
GET |
/api/mangas/search?q= |
Typo-tolerant library search via Meilisearch (title + title_original); 503 if MEILI_HOST isn't set — the frontend falls back to client-side substring matching in that case |
Project Structure
backend/
server.js Express API
db.js Knex query layer (async) — SQLite for dev, Postgres in production
db-config.js Shared Knex connection config
knexfile.js Wrapper around db-config.js for the `knex` CLI
migrations/ Knex migration files
schemas.js Zod input validation
frontend/src/
api.ts Typed fetch wrappers
types.ts Shared TypeScript types
hooks/ Business logic hooks
components/ UI components
context/ MangaContext (users, currentUserId, colorOf)
locale/ i18n strings (de/en)
e2e/ Playwright tests
Features
- Track owned volumes per manga series
- Per-user reading status
- Wishlist per user
- Buy list (volumes needed to complete a series)
- Up Next view (what to read next)
- Reading history
- Hide manga per user
- Borrowed volumes tracking
- Multi-user with colour coding
- PWA support
- German/English UI