A self-hosted, fully local Goodreads alternative.
Scan ISBN barcodes · fetch covers & metadata automatically · keep your reading life private.
Latest release: v1.12.0 · scan-integrity hardening · batch scanning · custom cover photos · installable Android APK
PageVault is a book catalogue that runs entirely on your own hardware. Point a camera at any ISBN barcode and it fetches the title, author, cover, and metadata, then stores everything in a single SQLite file that never leaves your device. Think Goodreads, but yours, with no account and no cloud.
The same Flask app powers three builds: a web server, a native desktop app, and an Android app. All three share one codebase and one feature set.
Made with love for my wife Emili. ❤️
| Build | Best for | How |
|---|---|---|
| Web server | Any device on your network, self-hosting, Docker | python app.py, or docker compose up |
| Desktop app (Windows) | A double-click program, no terminal | Install the .exe from Releases |
| Android app | A phone app that runs fully offline on-device | Install the APK from Releases, or build android/ yourself (guide) |
The web and desktop builds also let a phone scan barcodes over the LAN; the Android build puts the whole app on the phone, using its camera directly with no network needed (except to look up metadata).
Scanning & metadata. Scan an ISBN with the camera (or type it) and PageVault fills in title, author, cover, publisher, year, page count, genre, language, and community rating. Every ISBN runs through an ordered pipeline of free providers — Crossref, Open Library (search and editions), the Library of Congress (English-group ISBNs), Google Books, and the Deutsche Nationalbibliothek (German-group 978-3) — where each source fills gaps and richer ones refine what earlier ones found. A lookup popup shows a loading bar and a found / not found result; auto-filled title, author and year stay editable before you add the book. On Android, scanning uses Google's on-device ML Kit barcode scanner (with the in-page scanner as fallback). Books without a real ISBN are resolved by title/author.
Batch scanning & custom covers. Batch scan keeps the camera open and collects ISBNs without stopping, looking each up in the background; a fully editable review list (title, author, year and status per book) is then added in one go. Any book can also be given a cover from a photo or an uploaded image, cropped to the standard 2:3 book-cover shape and stored on-device.
Reading tracking. Status (want to read / reading / read / DNF), per-review page progress with a live progress bar, reading sessions (pages + time), annual goals with pace and streak metrics, re-read history with dates, series name/number, and format (physical / e-book / audiobook) with owned/wishlist state.
Reviews, ratings & quotes. Half-star ratings (0.5–5.0), written notes over time, and saved quotes with page numbers.
Built-in e-book reader. Attach an EPUB or PDF to any book and read it in-app, in a
full-screen dialog or on the dedicated /reader page. Position is saved automatically and
folded into page progress. An OPDS feed (/opds) also lets external e-reader apps
(KOReader, Moon+ Reader) browse and download your library.
Organisation, search & discovery. Custom shelves (with optional logos), genre-tag chips (deduped, max 10), filtering by status/author/genre/shelf, full-text search across book metadata, your review notes, and saved quotes (SQLite FTS5), and local similarity recommendations computed from your own library, no external service.
Stats dashboard. /stats renders 20+ interactive Plotly charts: books/pages by status,
top genres and authors, rating distribution, monthly activity, format and decade breakdowns,
publisher insights, community-vs-personal ratings, reading pace and speed, a GitHub-style
activity heatmap, library growth, rating and genre trends, time-to-finish, shelf breakdown,
and active loans. Preset and custom date ranges; inherits your light/dark theme.
Import / export / backup. CSV export and import (Goodreads My Books format supported,
with mapping preview and dry-run), full JSON export, and a one-click ZIP backup / validated
restore. Large imports run as background jobs with a progress bar and are idempotent.
Interface. English / German toggle and light / dark library themes, both remembered locally. Responsive layout with native-app polish on the Android build.
Fully local & offline-capable. Your whole library is one pagevault.db file. All
front-end libraries and fonts are vendored (no CDN at runtime), and the app shell and book
covers are cached locally (a service worker plus an on-disk cover cache), so everything
except online metadata lookup works offline.
Requires Python 3.10+.
git clone https://github.com/ChristianAbele02/PageVault.git
cd PageVault
python -m venv .pagevault
.\.pagevault\Scripts\activate # Windows; source .pagevault/bin/activate on macOS/Linux
pip install .
python app.pypython app.py serves over HTTPS by default so phones can use the camera scanner
(browsers only allow the camera on a secure origin). The banner prints the local and
same-Wi-Fi URLs and, unless PAGEVAULT_ADMIN_PASSWORD is set, a one-time admin password.
Open https://localhost:5000 and accept the one-time self-signed-certificate warning.
Don't need the phone scanner? PAGEVAULT_HTTPS=0 python app.py serves plain HTTP.
Copy
.env.exampleto.envand setSECRET_KEYandPAGEVAULT_ADMIN_PASSWORDbefore exposing PageVault to your network.
docker compose up -d # http://localhost:5000, data in a named volumeDownload PageVault-Setup-<version>.exe from the
latest release and launch from the
Start menu; it opens in its own WebView2 window. The installer is per-user (no admin rights),
data lives in %LOCALAPPDATA%\PageVault, and a portable ZIP is also provided. Phone scanning
works: the app runs a LAN HTTPS server, so Mobile shows a QR the phone can open with a
working scanner. Build from source with make exe (see Development).
Download PageVault-<version>.apk from the
latest release onto your phone,
open it, and allow installing from unknown sources when Android asks (the app is not on the
Play Store). Everything runs on-device: the Flask app runs on a loopback port inside the
app via embedded CPython (Chaquopy) and the UI renders in a WebView, so the catalogue,
camera scanner, reader, stats, import/export, and backups all work locally with no server.
Admin login is omitted. To build from source instead, open the android/ folder in Android
Studio and Run — see android/README.md.
Browsers expose the camera only on a secure origin (localhost or HTTPS). A phone reaches
the web/desktop build over the LAN, so those serve HTTPS with a self-signed certificate
generated on first launch under your data directory (git-ignored, reused across restarts,
covering localhost and your LAN IP). Accept the one-time warning per device, or trust a
mkcert certificate to remove it:
mkcert -install
mkcert -cert-file certs/pagevault-cert.pem -key-file certs/pagevault-key.pem localhost 127.0.0.1 192.168.x.xThe Android build needs none of this: http://127.0.0.1 is already a secure origin, so the
on-device camera works with no certificate. Behind a reverse proxy or Docker, TLS is
terminated upstream and this does not apply.
| File / folder | What it is |
|---|---|
pagevault.db |
Your entire library — books, reviews, shelves, tags, goals, sessions. Back this up. |
book_files/ |
Uploaded e-book files (EPUB/PDF), one per book. |
pagevault.log |
Rotating log (10 MB × 5). Safe to delete. |
From a source checkout these sit next to app.py. The desktop app keeps them (plus
secret_key and admin_password.txt) in %LOCALAPPDATA%\PageVault; the Android app keeps
them in its private storage. Set PAGEVAULT_DATA_DIR to override the location. Nothing is
sent anywhere except the metadata providers you look ISBNs up against.
Admin panel (web/desktop only): visit /admin/login. The password is printed on startup
(or saved to admin_password.txt in the desktop app) unless PAGEVAULT_ADMIN_PASSWORD is
set. Login is rate-limited (5 failures per address → HTTP 429 for 5 minutes).
All responses are JSON. Base URL is http://localhost:5000 under Docker/gunicorn/PAGEVAULT_HTTPS=0,
otherwise https://localhost:5000 (add curl -k).
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/books |
List books — ?status=&author=&genre=&shelf_id=&q=&sort=&order= |
POST |
/api/books |
Add a book { isbn, status?, genre_tags?, shelf_ids?, book_data? } |
GET PATCH DELETE |
/api/books/:id |
Book detail / update / delete |
GET |
/api/books/:id/recommendations |
Similar books from your library (?limit=) |
GET |
/api/search |
Full-text search across metadata, review notes, and quotes (?q=) |
GET POST DELETE |
/api/books/:id/reviews[/:rid] |
Reviews { rating?, comment?, current_page? } |
GET POST DELETE |
/api/books/:id/quotes[/:qid] |
Quotes with page numbers |
GET POST DELETE |
/api/books/:id/reads[/:rid] |
Re-read history |
POST GET DELETE |
/api/books/:id/file |
Upload / stream / remove the e-book file |
PATCH |
/api/books/:id/position |
Save reader position { cfi?, percent?, current_page? } |
POST |
/api/books/:id/sessions · GET /api/sessions |
Reading sessions |
GET POST PATCH DELETE |
/api/shelves[/:id] |
Custom shelves |
GET |
/api/lookup/:isbn |
Preview ISBN metadata without saving |
POST |
/api/books/refresh[/start] |
Refresh metadata for all books (preserves reviews/tags/shelves) |
POST |
/api/metadata/repair[/start] · GET /api/metadata/jobs[/:id] |
Repair jobs + progress |
GET PUT |
/api/goals/current |
Yearly reading goal + progress |
GET |
/api/stats · /api/stats/analysis |
Statistics + plot-ready dataset (?start_date=&end_date=) |
GET |
/api/export · /api/export/csv |
Full library export (JSON / CSV) |
POST |
/api/import/csv[/preview][/start] |
Import PageVault or Goodreads CSV (preview / sync / background) |
GET POST |
/api/backup/download · /api/backup/restore/{validate,apply} |
Backup / restore |
POST |
/api/admin/{login,logout} · GET /api/admin/{diagnostics,logs} |
Admin session + diagnostics |
GET |
/api/mobile/connect |
Same-network URL for the mobile QR |
GET |
/opds |
OPDS 1.2 acquisition feed of books with e-book files (for e-reader apps) |
# Add by ISBN (metadata fetched automatically), then review it
curl -X POST http://localhost:5000/api/books -H "Content-Type: application/json" \
-d '{"isbn": "9780451524935", "status": "read"}'
curl -X POST http://localhost:5000/api/books/1/reviews -H "Content-Type: application/json" \
-d '{"rating": 5, "comment": "Essential reading."}'Lookups start with the Open Library Books API; if fields are missing, Google Books, Open
Library Search, Crossref, and (for 978-3 ISBNs) the DNB run in parallel, then merge
progressively so good data is never discarded. Community ratings come from Open Library's
CC0 ratings (keyless) with Google Books as backup; series info is Google-Books-only. Results
are cached in-process (TTL PAGEVAULT_LOOKUP_CACHE_TTL_SECONDS, default 900 s).
Google Books has a small keyless quota; bulk jobs throttle and pause after an HTTP 429. Set a
free PAGEVAULT_GOOGLE_BOOKS_API_KEY for higher coverage, then run Tools → Repair missing
metadata. SQLite runs in WAL mode with busy_timeout so the desktop build's two servers
never collide on a write.
pip install ".[dev,prod]" # or: make dev
python -m pytest # 132 tests; make test
python -m ruff check . # lint; make lint
python -m mypy app.py desktop.py config.py pagevault_coreCI (GitHub Actions) runs tests, ruff, mypy, and a Docker build. The Makefile wraps the same
commands. Front-end libraries are vendored under static/vendor (no CDN at runtime); the
desktop build freezes with PyInstaller (make exe); the Android build embeds CPython via
Chaquopy.
pagevault/
├── app.py · config.py · desktop.py Web factory · config/data dirs · desktop launcher
├── pagevault_core/
│ ├── api.py REST blueprint (all routes)
│ ├── db.py · metadata.py · utils.py SQLite · multi-provider lookup · helpers
│ ├── tls.py Self-signed cert for local HTTPS (phone scanning)
│ └── services/ admin_service.py · recommendations.py
├── templates/ index · stats · reader · admin (Jinja2)
├── static/ PWA manifest · service worker · i18n.js · vendor/
├── android/ On-device Android app (Chaquopy + WebView)
├── tests/ 186 tests (API · metadata · utils · TLS)
├── Dockerfile · docker-compose.yml Multi-stage, non-root, gunicorn
├── Makefile · pyproject.toml Tooling and packaging
└── CHANGELOG.md · CONTRIBUTING.md · SECURITY.md
- Built-in e-book reader (EPUB/PDF) with position sync
- English/German interface · annual goal tracker · admin console
- Mobile QR connect · local recommendations · desktop app
- Offline front-end (vendored libraries, cover cache)
- Android app: on-device release with installable APK
- Full-text search (FTS5) · OPDS catalogue feed for e-reader apps
- Goodreads import mapping presets (regional variants)
Have an idea? Open a feature request.
Contributions welcome — see CONTRIBUTING.md. Report security issues privately per SECURITY.md. Licensed under MIT.
Built with Flask ·
SQLite ·
Open Library