MechaCat02 57364fae32 chore: release-prep docs, env vars, compose, and e2e port hygiene
- README rewritten end-to-end: stack, quick start, dev workflow, full
  /api/v1 endpoint table, error and pagination envelopes, auth
  quick-start (browser + bot bearer), configuration table, deployment
  notes, backup/restore pointer. Stale "next features" section dropped
  now that all eight feat branches are in.
- .env.example now lists every env var the backend reads, with
  inline explanations:
  - COOKIE_SECURE / COOKIE_DOMAIN / SESSION_TTL_DAYS (auth)
  - CORS_ALLOWED_ORIGINS (same-origin by default)
  - MAX_REQUEST_BYTES / MAX_FILE_BYTES (upload caps)
  - Postgres + storage + log vars carried over.
- docker-compose.yml forwards all of the above into the backend
  service with `${VAR:-default}` so an unset value falls back to the
  same default the code uses, and any `.env` override flows through
  without a compose edit.
- docs/backup.md: step-by-step backup, restore, and smoke-test drill
  for both stateful volumes (postgres-data + storage-data), plus a
  list of what's deliberately *not* in the backup (e.g., .env).
- playwright.config.ts: pins the e2e dev server to port 5174 with
  `--strictPort` so it neither reuses nor silently bumps off
  collision with another vite instance on 5173. Drops the flaky
  manual-start workflow the earlier branches needed.
- docker-compose syntax (both prod and dev) validates cleanly against
  .env.example with no undefined-variable warnings.

No version bump — this is documentation, config, and tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:58:49 +02:00
2026-05-16 21:05:16 +02:00
2026-05-16 21:05:16 +02:00
2026-05-16 21:05:16 +02:00

Mangalord

A self-hosted manga and comics reader. Users browse, search, read, bookmark, and upload manga. The same HTTP API is consumed by the SvelteKit web UI and by external bots — there is no separate "bot API" — so anything you can do in the browser you can also do over curl with a bearer token.

Stack

  • Backend: Rust, axum 0.7, sqlx 0.8, Postgres 16. Argon2id passwords, HttpOnly SameSite=Lax session cookies, bearer tokens for bots.
  • Frontend: SvelteKit 2 with Svelte 5 runes, TypeScript.
  • Storage: pluggable Storage trait. LocalStorage ships in-tree; S3 and friends are first-class extension slots.
  • Deploy: a single host via Docker Compose.

Quick start

cp .env.example .env
docker compose up --build
Service URL
Frontend http://localhost:3000
API base http://localhost:8080/api/v1
Health check http://localhost:8080/api/v1/health

The first boot runs the migrations automatically. From there:

  1. Open http://localhost:3000, click Register, create a user.
  2. Click Upload to create a manga (optionally with a cover) and upload a chapter.
  3. Open the manga, click the chapter, read with j/k or the arrow keys.

Local development

For fast iteration, run Postgres in Docker and the backend + frontend natively:

docker compose -f docker-compose.dev.yml up -d

# Backend (separate shell)
cd backend
export DATABASE_URL=postgres://mangalord:mangalord@localhost:5432/mangalord
cargo run

# Frontend (separate shell)
cd frontend
npm install
npm run dev   # serves on http://localhost:5173, proxies /api to :8080

Port note: docker-compose.dev.yml exposes Postgres on 5432. If another Postgres is already bound to that port (or another Docker container is), stop it first or edit the host-port mapping in the dev compose file. Backend tests need a real Postgres that the test user can CREATEDB on; the dev container works because the mangalord role is the cluster superuser.

Tests

This project is developed test-first. Three levels:

# Backend: unit (in-module) + integration (#[sqlx::test] against a real DB)
cd backend && cargo test
cd backend && cargo clippy --all-targets -- -D warnings

# Frontend: Vitest unit tests and svelte-check
cd frontend && npm test
cd frontend && npm run check

# End-to-end: Playwright (mocks the backend at the page level by default)
cd frontend && npm run test:e2e

Backend integration tests provision a fresh, migrated database per test via sqlx::test, so they require DATABASE_URL to point at a Postgres where the configured user has CREATEDB.

Playwright starts the SvelteKit dev server on a dedicated port (5174) so it doesn't reuse whatever you may have running on 5173. To point Playwright at an already-running deployment instead, set E2E_BASE_URL=http://your.host.

API surface

Everything is namespaced under /api/v1/. /api/* outside the version prefix is reserved for future versioning.

Read endpoints (public)

Method Path Description
GET /api/v1/health Liveness probe.
GET `/api/v1/mangas?search=&sort=recent title&limit=&offset=`
GET /api/v1/mangas/{id} Single manga.
GET /api/v1/mangas/{id}/chapters Paginated chapter list, ordered by number.
GET /api/v1/mangas/{id}/chapters/{n} Single chapter.
GET /api/v1/mangas/{id}/chapters/{n}/pages Page metadata (storage keys + MIME).
GET /api/v1/files/{key} Streams a blob (cover or page).

Write endpoints (require authentication)

Method Path Description
POST /api/v1/auth/register {username,password} → user + sets session cookie.
POST /api/v1/auth/login Same shape; rotates to a fresh session.
POST /api/v1/auth/logout Invalidates the session, clears the cookie.
GET /api/v1/auth/me Current user, or 401 if anonymous.
POST /api/v1/auth/tokens {name} → bot bearer token (returned once).
DELETE /api/v1/auth/tokens/{id} Revoke a bot token (owner-only).
POST /api/v1/mangas Multipart: metadata JSON + optional cover image.
POST /api/v1/mangas/{id}/chapters Multipart: metadata JSON + N page image parts.
POST /api/v1/bookmarks {manga_id, chapter_id?, page?}.
DELETE /api/v1/bookmarks/{id} Remove a bookmark (owner-only).
GET /api/v1/me/bookmarks?limit=&offset= The caller's bookmarks.

Envelopes

Errors — every non-2xx response:

{ "error": { "code": "snake_case", "message": "human readable", "details": { "field?": "..." } } }

details is only present on validation_failed (422). code is the stable contract; the message is for humans and may change.

HTTP code
400 invalid_input, bad_file_key
401 unauthenticated
403 forbidden
404 not_found
409 conflict
413 payload_too_large
415 unsupported_media_type
422 validation_failed
500 internal_error

Lists — every paginated endpoint:

{
  "items": [ /* ... */ ],
  "page": { "limit": 50, "offset": 0, "total": 137 }
}

total is a number when the endpoint computes it (currently /mangas), otherwise null.

Auth quick-start

Browser

The frontend handles this for you: register → cookie set → writes work. Cookies are HttpOnly; SameSite=Lax; Secure (configurable via COOKIE_SECURE).

Bots / scripts (bearer tokens)

# 1. Log in once via cookies (or register).
curl -sb -c cookies.txt -X POST http://localhost:8080/api/v1/auth/login \
  -H 'content-type: application/json' \
  -d '{"username":"alice","password":"hunter2hunter2"}'

# 2. Mint a long-lived bot token. The `bearer` value is shown ONCE.
curl -sb cookies.txt -X POST http://localhost:8080/api/v1/auth/tokens \
  -H 'content-type: application/json' \
  -d '{"name":"ci-bot"}'
# → { "id": "...", "name": "ci-bot", "bearer": "raw-token-here", ... }

# 3. Use the bearer from anywhere.
curl -H 'Authorization: Bearer raw-token-here' \
     http://localhost:8080/api/v1/auth/me

Tokens are stored hashed (sha256) at rest; the raw value never leaves the response that created it. Revoke with DELETE /api/v1/auth/tokens/{id} while authenticated as the owner.

Configuration

All variables can be set in .env (for docker compose) or your shell (for cargo run). See .env.example for the full list with explanations.

Variable Default Purpose
DATABASE_URL required Postgres connection string.
BIND_ADDRESS 0.0.0.0:8080 Backend listen address.
STORAGE_DIR ./data/storage Local-storage root.
RUST_LOG info,mangalord=debug tracing-subscriber env filter.
COOKIE_SECURE true Secure flag on the session cookie.
COOKIE_DOMAIN (none) Scope the session cookie to a parent domain.
SESSION_TTL_DAYS 30 Session lifetime.
CORS_ALLOWED_ORIGINS (empty → same-origin) Comma-separated origin allowlist.
MAX_REQUEST_BYTES 209715200 (200 MiB) Hard cap on multipart request size.
MAX_FILE_BYTES 20971520 (20 MiB) Cap on a single image part.
PUBLIC_API_BASE http://localhost:8080/api Browser-facing API base.

Deployment

For real hosts:

  • Front Mangalord with a TLS terminator (Caddy, nginx, traefik). Point :443 at the frontend on :3000 and proxy /api/* to the backend on :8080. With same-origin routing you can leave CORS_ALLOWED_ORIGINS empty and the session cookie's SameSite=Lax does its job.
  • Set a strong Postgres password in .env before the first docker compose up. The defaults are fine for local dev only.
  • Keep COOKIE_SECURE=true behind HTTPS. Browsers drop Secure cookies on plain HTTP; the dev compose accepts COOKIE_SECURE=false for that case.
  • Watch RUST_LOG if you're noisy with debug — drop the ,mangalord=debug suffix in production to log at info for the app's spans.

Backup and restore

Two stateful volumes:

  • postgres-data — all metadata (users, mangas, chapters, pages, bookmarks).
  • storage-data — the actual cover and page bytes.

A consistent backup needs both. See docs/backup.md for step-by-step procedures and a restore drill.

Project layout

backend/      # axum + sqlx, see backend/src/ for the hexagonal layout
frontend/     # SvelteKit 2 / Svelte 5
docs/         # operator docs (backup procedure, etc.)
CLAUDE.md     # contributor playbook

The contributor guidelines (TDD, Conventional Commits, lockstep SemVer, hexagonal seams) are in CLAUDE.md.

Description
No description provided
Readme 702 KiB
Languages
Rust 59.1%
Svelte 21.9%
TypeScript 18.1%
CSS 0.6%
Dockerfile 0.2%
Other 0.1%