* fix(kobo): stop offering plugin updates to devices that crash applying them Plugin 1.3.x calls Device:unpackArchive, which KOReader removed in koreader/koreader@751b4978 (July 2026). The call is unguarded and the whole apply path runs without a pcall inside a ConfirmBox callback, so the missing method raises on UIManager's event loop and takes KOReader down. Plugin 1.4.0 fixed the extraction, but the broken updater is the code that would have to install that fix, so it can never self-deliver: every device still on 1.3.x crashes on every update attempt, forever. Gate the offer server side instead. GET /koreader/plugin/version now resolves the calling user and withholds pluginVersion when any of their devices last reported a plugin older than 1.4.0. Those clients already treat "unknown" as "the server could not tell me" and show a message rather than the update prompt, so the crashing path becomes unreachable without touching the plugin on the device. Two limits are inherent to a server-only mitigation and are not bugs. The endpoint carries no device id, only the user, so one stale device parks self-update for all of that user's devices until it is updated by hand or removed. A device that has never completed a sweep has no recorded version, so it is not gated. Neither is closable until the plugin reports its own version on the version check. Surface requiresManualUpdate per device in the KOReader settings so the affected devices say what to do, since the 1.3.x dialog string is baked into the installed plugin and cannot be changed from here. Closes #905 * fix(kobo): keep blank plugin versions inside the self-update gate A device reporting an empty pluginVersion produced a falsy blocker, so the truthiness check opened the gate and handed out the real version. The DTO allows an empty string, so this was reachable. Also move the settings spec fixtures above the manual-update cutoff, since a sub-1.4.0 version with requiresManualUpdate false describes a state that cannot occur.
BookOrbit - Server
NestJS 11 backend running on Fastify.
Running
From the repo root: pnpm dev (starts both server and client).
Server only: pnpm start:dev (from this directory).
The server loads environment variables from .env in this directory. See .env.example for all available options.
API docs
OpenAPI docs are disabled by default. Enable them with SWAGGER_ENABLED=true:
SWAGGER_ENABLED=true pnpm start:dev
Then open:
- Swagger UI: http://localhost:3000/api/docs
- OpenAPI JSON: http://localhost:3000/api/docs-json
Dev watch uses a Nest config without the Swagger metadata generator so the
server does not restart immediately after startup. Full build output still uses
the standard Nest config and generates src/metadata.ts for enriched docs.
Module structure
Each feature is a self-contained NestJS module under src/modules/:
src/modules/
├── auth/ Authentication (JWT, OIDC, password reset)
├── book/ Book CRUD, cover images, file serving
├── library/ Library management
├── scanner/ File system scanning, metadata extraction
├── collection/ User book collections
├── smart-scope/ Saved search/filter views
├── user/ User management
├── role/ Roles and permissions (RBAC)
├── metadata/ Book metadata providers
├── kobo/ Kobo device sync
├── annotation/ Highlights and annotations
├── bookmark/ Reading position bookmarks
├── reader-preferences/ Per-user reader settings
├── cbz/ CBZ archive handling
├── path/ File path utilities
├── seed/ Database seeding
└── app-settings/ Global application settings
Database
Schema files live in src/db/schema/, one per domain. Migrations are generated by Drizzle Kit.
pnpm db:generate add_some_column # generate migration from schema diff
pnpm db:migrate # apply pending migrations
pnpm db:studio # open Drizzle Studio (DB browser)
Testing
pnpm test # unit tests
pnpm test:watch # unit tests in watch mode
pnpm e2e:run # app-smoke end-to-end test from server package
pnpm test:cov # unit tests with coverage
For suite-based e2e runs, run from the repository root:
pnpm run e2e:list
pnpm run e2e:all
pnpm run e2e:run -- scanner-scenarios
Quality
pnpm type-check # TypeScript checks (no emit)
pnpm lint:check # Non-mutating lint checks
pnpm lint:fix # Auto-fix lint issues
pnpm format:check # Prettier check
pnpm format # Prettier write
Key conventions
- Constructor injection with
@Inject(DB)for the Drizzle instance. - Config via
@nestjs/confignamed configs (appConfig,dbConfig,authConfig,storageConfig,mailerConfig). Never readprocess.envdirectly in services. - DTOs use
class-validatordecorators. GlobalValidationPiperejects unknown fields. - Throw NestJS
HttpExceptionsubclasses (NotFoundException,ForbiddenException, etc.) - never rawError. - All user-owned data is scoped by
userId. Service methods check ownership.