Files
bookorbit/server
neon b1ba3a03e7 fix(koreader): stop a stale device position undoing a progress reset
Clearing a book's progress deleted the shared row and left the KOReader device row behind, so
the next pull served the deleted position straight back. Clearing that row too is not enough
either: an empty response reads as "this server knows nothing about your book", which is the
one reply that leaves a device certain its own sidecar is the only position there is, and it
pushes that back on the next sync.

Record the reset as a durable fact and answer the pull with a real start position attributed to
BookOrbit. Route the position by format, because KOReader's own kosync plugin feeds it straight
to GotoPage or GotoXPointer with no percentage fallback and an empty value leaves the reader
where it was. Stamp each delivery as current, because both clients compare that against their
last page turn and default backward syncs to disabled, so the original reset time would be
dropped on any pull deferred past a page turn without ever prompting.

Serving a reset proves nothing, since either client can classify it as a backward sync and drop
it silently, so the marker survives delivery and a device is held until its own push reports the
start. Retire it once a device that took the reset reads on: the pull carries no device
identity, so a marker kept alive past its purpose is answered to every device and would send
that one back to the start on every sync. The bound this leaves is deliberate. A second device
that never took the reset is protected only until the first reads on, and closing that needs
device_id on the pull, which is a plugin change.

Judge convergence on the reported position rather than the percentage, so page 1 of a hundred
page comic is not mistaken for a position no short book could ever reach. Wind Kobo bookmarks
back through the same paths, since a Kobo device otherwise resumes from its own bookmark and
reports it back the same way.

Closes #1032
2026-08-18 21:29:36 +00:00
..

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:

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/config named configs (appConfig, dbConfig, authConfig, storageConfig, mailerConfig). Never read process.env directly in services.
  • DTOs use class-validator decorators. Global ValidationPipe rejects unknown fields.
  • Throw NestJS HttpException subclasses (NotFoundException, ForbiddenException, etc.) - never raw Error.
  • All user-owned data is scoped by userId. Service methods check ownership.