Project · 2026
Bewks
A self-hosted digital library for managing and reading ebooks and audiobooks, with request workflows, Goodreads metadata sync, and cloud storage.
- TypeScript
- Next.js
- Prisma
- SQLite
- Cloudflare R2
- BullMQ
- NextAuth.js
Project snapshot
- Status
- Live self-hosted deployment for a private library.
- Deployment
- Private deployment with flexible local and cloud-backed media storage.
- Data model
- Ebooks, audiobooks, request workflows, series metadata, member roles, and a newer film/TV media module.
- Quality
- 635 Vitest unit/component tests and 27 Playwright E2E spec files, run against isolated per-worker databases.
Technical proof
- Strict route/controller/service/repository layering with dependency-injected interfaces.
- BullMQ background jobs enrich records through Goodreads sync, R2 storage, and scraping fallbacks.
- Role-aware access, CSRF protection, rate limiting, and structured audit logs.
What it is
Bewks is a personal self-hosted library system for ebooks and audiobooks. It handles the full lifecycle of a digital collection: scanning local directories for EPUB, MOBI, and PDF files, extracting and normalizing metadata, organizing books by series and author, and serving them through a web UI with an in-browser reader. It is built for a single owner or small household, with role-based access (OWNER, ADMIN, MEMBER, GUEST) and a request workflow that lets members ask for titles to be added.
Architecture tour
Layering and data flow
A request enters through an app/api route, which is thin by design: it parses input, checks auth, and hands off. From there it goes route → controller → service → repository, with a TypeScript interface at each boundary (IAuth.service.ts and siblings under src/interfaces) so a service can be swapped or mocked without touching the controller above it. src/repositories is the only layer that talks to Prisma directly. There are 246 route files under src/app/api and a matching src/controllers / src/services / src/repositories split, so the shape isn’t a diagram, it’s the actual file layout. The schema itself has grown to 45 Prisma models, covering books, series, requests, users, media (film/TV), and audit records, across 56 migrations, all on SQLite.
File storage is dual-mode: a book’s bytes live on local disk or in Cloudflare R2, chosen per-library, with presigned URLs for delivery so the app server never proxies file bytes it doesn’t have to. Background work runs through BullMQ on Redis: the goodreadsSync worker is a standalone process (its own pm2 entry) that pulls jobs off a queue and calls the Goodreads service to enrich book and series records with covers and metadata.
A couple of decisions and why
The interface-at-every-boundary layering is more ceremony than a solo project strictly needs, but it’s what let the Goodreads sync move from a single scraper call to a full queue-based worker without rewriting the controllers that trigger it. When Goodreads started blocking plain HTTP clients, the fallback was a headless Playwright fetch behind the same service interface, so nothing upstream had to change. Storage being dual-mode (local disk or R2) instead of picking one exists because the deployment target itself is dual-mode: a home server most of the time, with R2 as the cloud fallback when disk space or bandwidth runs out.
Auth, deploy tooling, and DB backup are not reinvented per app: they come from a family of private @andrewpopov/*-kit packages (auth-kit, authz-kit, deploy-kit, admin-kit, mailer-kit, and others) shared across several of my apps, so a fix to session handling or the deploy pipeline lands once and gets pulled in everywhere instead of drifting between repos.
Testing
Unit and integration tests run under Vitest (635 .test.ts/.test.tsx files across src), with React Testing Library for components. End-to-end coverage is Playwright: 27 spec files under e2e/, run against parallel, isolated per-worker databases so E2E runs don’t collide with each other. A separate prod-smoke Playwright config runs a smaller smoke suite against the live deployment after release.
Where it’s at
A recent addition extended the library beyond books to include a media module backed by TMDB, IMDb (via GraphQL), and Rotten Tomatoes for film and TV metadata, with user-created watchlists and status tracking.