The Installation section assumed 'git clone the repo'. Self-hosted operators only need three files. The install is now: mkdir + copy this compose block + .env (three openssl secrets) + uploads dir + pull/up. The compose is fully self-contained: the runtime-role bootstrap is inlined into db-init's command (same SQL as database/schema/z_rls_role.sh — keep in sync) — no ./database/schema bind mount, because the app itself executes the schema as the runtime role (owner) on first boot; with the role as DATABASE owner the trusted pg_trgm extension needs no superuser. Verified end to end in a throwaway project: fresh volume, pasted block, .env with generated secrets, db-init converged role+ownership, app ran the full schema and came up healthy — no repo checkout anywhere. Compose-quoting lessons encoded (the reason for odd dollars): compose interpolates '532016' to '$' before the container sees it, so the plpgsql 532016 tags in the inline SQL are written 532016532016, and PGUSER must be set explicitly (POSTGRES_USER is not a libpq variable).
313 lines
11 KiB
Markdown
313 lines
11 KiB
Markdown
# <img src="web/static/favicon.svg" width="32" alt="Bookhoard logo"> Bookhoard
|
|
|
|
A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and Tailwind CSS featuring **universal cross-device sync**, beautiful dark themes, and comprehensive media management.
|
|
|
|
## ✨ Why Bookhoard?
|
|
|
|
**🔄 Universal Sync**: Your reading position, bookmarks, highlights, and notes sync automatically between KOReader and the web - with native Kobo sync and mobile apps coming later.
|
|
|
|
**📱 Multi-Library**: Organize your ebooks, comics, and manga with per-library folders and smart collections.
|
|
|
|
**🎨 Beautiful UI**: 11 gorgeous dark themes with responsive design that works on any device.
|
|
|
|
**🔒 Secure**: JWT authentication, bcrypt password hashing, rate limiting, and no passwords on devices.
|
|
|
|
---
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### Prerequisites
|
|
|
|
- **Podman** (recommended) or Docker
|
|
- **5 minutes** of your time
|
|
|
|
### Installation (no source checkout needed)
|
|
|
|
Create a directory on the Docker host. You only need the compose file,
|
|
an `.env`, and an `uploads` folder — everything else rides the published
|
|
image.
|
|
|
|
```bash
|
|
mkdir bookhoard && cd bookhoard
|
|
mkdir uploads
|
|
```
|
|
|
|
**`docker-compose.yml`** — copy this wholesale:
|
|
|
|
```yaml
|
|
services:
|
|
db:
|
|
image: postgres:15-alpine
|
|
container_name: bookhoard_db
|
|
environment:
|
|
POSTGRES_DB: bookhoard
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: ${DBPASS}
|
|
PGPORT: 5432
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
- ./uploads:/app/uploads
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
|
|
db-init:
|
|
image: postgres:15-alpine
|
|
container_name: bookhoard_db_init
|
|
environment:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_DB: bookhoard
|
|
PGHOST: db
|
|
PGUSER: postgres
|
|
PGPASSWORD: ${DBPASS}
|
|
DBSRVPASS: ${DBSRVPASS}
|
|
entrypoint: ["bash", "-c"]
|
|
command:
|
|
- |
|
|
set -e
|
|
: "${DBSRVPASS:?DBSRVPASS required in .env}"
|
|
psql -v ON_ERROR_STOP=1 -v role_pass="$DBSRVPASS" <<'EOSQL'
|
|
DO $$$$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bookhoard_srv') THEN
|
|
CREATE ROLE bookhoard_srv LOGIN;
|
|
END IF;
|
|
END $$$$;
|
|
ALTER ROLE bookhoard_srv LOGIN PASSWORD :'role_pass';
|
|
DO $$$$
|
|
DECLARE r RECORD;
|
|
BEGIN
|
|
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP
|
|
EXECUTE format('ALTER TABLE public.%I OWNER TO bookhoard_srv', r.tablename);
|
|
END LOOP;
|
|
FOR r IN SELECT sequencename FROM pg_sequences WHERE schemaname = 'public' LOOP
|
|
EXECUTE format('ALTER SEQUENCE public.%I OWNER TO bookhoard_srv', r.sequencename);
|
|
END LOOP;
|
|
FOR r IN SELECT p.oid::regprocedure AS fn FROM pg_proc p
|
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
WHERE n.nspname = 'public' AND p.prokind = 'f' LOOP
|
|
EXECUTE format('ALTER FUNCTION %s OWNER TO bookhoard_srv', r.fn);
|
|
END LOOP;
|
|
END $$$$;
|
|
ALTER SCHEMA public OWNER TO bookhoard_srv;
|
|
ALTER DATABASE bookhoard OWNER TO bookhoard_srv;
|
|
EOSQL
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
restart: "no"
|
|
|
|
app:
|
|
image: git.linuxhg.com/bookhoard/bookhoard:${IMAGE_TAG:-latest}
|
|
container_name: bookhoard
|
|
restart: unless-stopped
|
|
environment:
|
|
DATABASE_HOST: db
|
|
DATABASE_PORT: 5432
|
|
# Runtime role: bookhoard_srv (created above, confirmed by db-init).
|
|
# NEVER connect the app as the bootstrap superuser — it is subject
|
|
# to row-level security (per-user library visibility) by design.
|
|
DATABASE_USER: bookhoard_srv
|
|
DATABASE_PASSWORD: ${DBSRVPASS}
|
|
JWT_SECRET: ${JWT_SECRET}
|
|
SERVER_PORT: 8765
|
|
ports:
|
|
- "8765:8765"
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
db-init:
|
|
condition: service_completed_successfully
|
|
volumes:
|
|
- ./uploads:/app/uploads
|
|
- bookhoard_conversion_cache:/app/cache/kepub
|
|
|
|
volumes:
|
|
postgres_data:
|
|
bookhoard_conversion_cache:
|
|
```
|
|
|
|
**`.env`** — create alongside the compose file and fill in the three
|
|
secrets (no special characters):
|
|
|
|
```bash
|
|
openssl rand -hex 32 # JWT_SECRET
|
|
openssl rand -hex 16 # DBPASS (bootstrap database password)
|
|
openssl rand -hex 16 # DBSRVPASS (runtime role password)
|
|
```
|
|
|
|
Then start:
|
|
|
|
```bash
|
|
docker compose pull
|
|
docker compose up -d
|
|
open http://localhost:8765
|
|
```
|
|
|
|
Notes:
|
|
- `db-init` runs on every startup and converges the runtime role
|
|
(idempotent, ~1 second); the app refuses to start before it succeeds.
|
|
- `uploads/` on the host is where your book files live; libraries scan
|
|
folders under it from the Admin → Libraries page.
|
|
- First user to register automatically becomes an admin.
|
|
- Upgrades: `docker compose pull && docker compose up -d` (pin a version
|
|
with `IMAGE_TAG` in `.env`).
|
|
|
|
---
|
|
|
|
## 📖 Key Features
|
|
|
|
### Universal Cross-Platform Sync
|
|
|
|
- **Real-Time Progress**: Turn a page on your e-reader, see it in your browser
|
|
- **Format-Aware**: EPUB CFI, page numbers, percentages - all handled correctly
|
|
- **Offline Queue**: Changes sync when you reconnect, priority-processed
|
|
- **Conflict Resolution**: Smart handling when same book read on multiple devices
|
|
- **Book Matching**: Automatic matching using SHA-256, ISBN, UUID
|
|
- **OPDS Catalog**: Wireless book delivery to e-readers over Wi-Fi
|
|
- **Format Conversion**: On-the-fly EPUB→KEPUB conversion (for upcoming native Kobo support)
|
|
|
|
### Media Management
|
|
|
|
- **Calibre Integration**: Automatic metadata import from Calibre `metadata.opf` sidecar files
|
|
- **Smart Search**: Partial matching with fuzzy search fallback for typos
|
|
- **Advanced Filtering**: Filter by author, series, genre, language, year, cover images
|
|
- **Dynamic Sorting**: By title, author, date added, published date, page count, series
|
|
- **Rich Metadata**: Title, author, series, publisher, ISBN, language, edition, tags
|
|
- **5-Star Ratings**: Half-star precision (1-10 scale)
|
|
- **Notes & Highlights**: Color-coded annotations with linked notes
|
|
- **Usage Analytics**: Reading statistics, device usage, popular books
|
|
|
|
### Smart Collections
|
|
|
|
- **Auto-Assign Rules**: Automatically add books based on genre, author, series, tags, language, publisher, year
|
|
- **Device Shelf Mappings**: Map collections to device shelves (used by native Kobo sync, coming soon)
|
|
- **Test Before Creating**: Preview which books match your rules
|
|
|
|
### Library Organization
|
|
|
|
- **Multi-Library Support**: Ebooks, Comics, and Manga with type-specific file formats
|
|
- **Multiple Folders**: Add multiple scanning folders per library
|
|
- **Visibility Control**: Admins control which libraries each user can see
|
|
- **Background Scanning**: Auto-scan with per-user frequency settings
|
|
- **Watch Mode**: Real-time file system monitoring for instant updates
|
|
|
|
### Security
|
|
|
|
- **JWT Authentication**: Short-lived access tokens (1 hour) with refresh tokens (7 days)
|
|
- **Strong Passwords**: Complexity requirements enforced (8+ chars, uppercase, lowercase, number, special)
|
|
- **Account Lockout**: 5 failed attempts = 15-minute lockout
|
|
- **Rate Limiting**: 10 requests/minute on auth endpoints
|
|
- **Input Validation**: Comprehensive validation on all inputs
|
|
- **No Passwords on Devices**: Web-based device approval with QR codes
|
|
|
|
---
|
|
|
|
## 📚 Documentation
|
|
|
|
### For Users & Self-Hosters
|
|
|
|
- **[docs/user/calibre-integration.md](docs/user/calibre-integration.md)** - Calibre library integration
|
|
- **[docs/user/sync-guide.md](docs/user/sync-guide.md)** - Understanding and using universal sync
|
|
- **[docs/user/devices/koreader-setup.md](docs/user/devices/koreader-setup.md)** - KOReader configuration
|
|
- **[docs/user/devices/kobo-setup.md](docs/user/devices/kobo-setup.md)** - Kobo e-reader configuration (coming soon)
|
|
- **[docs/user/user-guide.md](docs/user/user-guide.md)** - General user guide
|
|
- **[docs/user/admin-guide.md](docs/user/admin-guide.md)** - Admin features and configuration
|
|
- **[docs/user/settings-guide.md](docs/user/settings-guide.md)** - Settings and preferences
|
|
|
|
### For Developers
|
|
|
|
- **[docs/developer/api/api-reference.md](docs/developer/api/api-reference.md)** - Complete API documentation
|
|
- **[docs/developer/android-app.md](docs/developer/android-app.md)** - Android app design & roadmap
|
|
- **[docs/contributing/development.md](docs/contributing/development.md)** - Development workflow
|
|
|
|
---
|
|
|
|
## 🎯 Supported Devices
|
|
|
|
| Platform | Sync | OPDS | Status |
|
|
| ---------------- | ---- | ---- | ------------------------------------------------------------- |
|
|
| **Web Browser** | ✅ | ✅ | Full support |
|
|
| **KOReader** | ✅ | ✅ | Runs on Kindle, Kobo, PocketBook hardware |
|
|
| **Kobo Devices** | 🚧 | 🚧 | Native Kobo sync coming soon (use KOReader on Kobo today) |
|
|
| **Mobile Apps** | 🚧 | 🚧 | Native Android app in design ([docs](docs/developer/android-app.md)); iOS later |
|
|
|
|
---
|
|
|
|
## 💾 Network Storage (NAS / NFS / SMB)
|
|
|
|
Libraries on network mounts are supported for everyday use — serving,
|
|
reading, progress, and annotation sync all work over the mount — with a
|
|
few behaviors worth knowing:
|
|
|
|
- **Outage hysteresis.** When the mount drops, files miss one scan
|
|
(marked missing, hidden) then a second (archived, still recoverable).
|
|
When the mount returns — by path or by identical content elsewhere —
|
|
everything is restored automatically: annotations, progress, and
|
|
covers included.
|
|
- **Retention purge.** Archived items older than `ARCHIVE_RETENTION_DAYS`
|
|
(default **90**, `0` = never auto-purge) are deleted for good by
|
|
library scans, annotations included. A NAS offline longer than the
|
|
retention window loses those books' metadata on return.
|
|
- **Deleting while the storage is down is refused.** Deleting a book
|
|
cascades its annotations, so deletes are rejected with a clear error
|
|
while the item's library storage is unreachable. Archive the item
|
|
instead, or retry when the mount is back.
|
|
- **Watch mode does not fire on network mounts** (inotify doesn't work
|
|
over NFS/SMB). The periodic scan is the change detector.
|
|
- **Latency** — folder reachability checks are cached ~30s; large
|
|
libraries on slow mounts will still feel slower than local disk.
|
|
|
|
---
|
|
|
|
## 🛠 Tech Stack
|
|
|
|
- **Backend**: Go 1.25+ with Echo framework
|
|
- **Database**: PostgreSQL 15+ with pgx v5
|
|
- **Frontend**: HTMX + Tailwind CSS + Templ
|
|
- **Auth**: JWT tokens with bcrypt password hashing
|
|
- **Container**: Podman (Docker compatible)
|
|
|
|
---
|
|
|
|
## 🧪 Testing
|
|
|
|
```bash
|
|
# Run all tests
|
|
make test-all
|
|
|
|
# Run integration tests (with test mode)
|
|
make test-integration
|
|
|
|
# Run Bruno OpenCollection YAML API tests
|
|
npm install -g @usebruno/cli
|
|
bruno run
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Project Status
|
|
|
|
**Version**: 1.0
|
|
**License**: AGPL-3.0
|
|
**Status**: Production-ready ✅
|
|
|
|
---
|
|
|
|
## 🤝 Contributing
|
|
|
|
We welcome contributions! Please see [docs/developer/development.md](docs/developer/development.md) for guidelines.
|
|
|
|
---
|
|
|
|
## 📄 License
|
|
|
|
AGPL-3.0 - See [LICENSE](LICENSE) file for details.
|
|
|
|
---
|
|
|
|
**Built with ❤️ using Go, PostgreSQL, HTMX, and Tailwind CSS**
|