No description
  • TypeScript 96.5%
  • JavaScript 2.2%
  • Dockerfile 1%
  • HTML 0.3%
Find a file
Freydis Mende 71a46b8420
All checks were successful
CI / Frontend – typecheck, lint, test & build (push) Successful in 31s
CI / Backend – install & typecheck (push) Successful in 1m16s
CI / Docker – build image (push) Successful in 15s
CI / deploy (push) Successful in 0s
Rename docker network from web to proxy, drop traefik mTLS labels
2026-08-30 22:07:59 +02:00
.forgejo/workflows Migrate backend to TypeScript, fix broken zod custom messages 2026-08-14 23:45:53 +02:00
.husky add husky with lint-staged 2026-05-31 22:52:44 +02:00
backend Show the client cert's CN value on /api/me 2026-08-30 21:19:05 +02:00
e2e Replace manual user picker with mTLS cert-derived identity 2026-08-01 16:00:21 +02:00
frontend Show the client cert's CN value on /api/me 2026-08-30 21:19:05 +02:00
.dockerignore Rename client to frontend, move backend files into backend/, harmonize CI 2026-08-14 10:22:30 +02:00
.gitignore Rename client to frontend, move backend files into backend/, harmonize CI 2026-08-14 10:22:30 +02:00
CLAUDE.md Migrate backend to TypeScript, fix broken zod custom messages 2026-08-14 23:45:53 +02:00
docker-compose.yml Rename docker network from web to proxy, drop traefik mTLS labels 2026-08-30 22:07:59 +02:00
Dockerfile Migrate backend to TypeScript, fix broken zod custom messages 2026-08-14 23:45:53 +02:00
package-lock.json Move data layer to Knex (SQLite for dev, Postgres-ready for prod) 2026-08-12 15:57:00 +02:00
package.json Migrate backend to TypeScript, fix broken zod custom messages 2026-08-14 23:45:53 +02:00
playwright.config.ts add e2e tests 2026-05-31 21:44:08 +02:00
README.md Add typo-tolerant library search via Meilisearch 2026-08-15 15:07:48 +02:00

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 whether DATABASE_URL is 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