Keep the narration marker a reading progress write leaves out. The save turned an omitted positionSeconds, mediaOverlayFragment or mediaOverlaySectionIndex into null and wrote it, so any client saving only its text position erased the marker another client had left. An explicit null still clears it. The audiobook-to-ebook sync now moves narrationUpdatedAt and the narration percentage with the marker it writes, so a comparison of stamps no longer prefers an older text position. Answer TTS requests as their types say. A position never saved is JSON null rather than an empty 200 body, and the book preferences PUT answers providerId, voiceId, speed and bookId, with the provider as a string. TtsPosition gains the updatedAt the route has always sent. Refuse with a 400 what used to reach the database as a 500: a null speed for the account (now left unchanged), a provider id that names no provider, a voiceId over 200 characters, a cfi, fragment or Kobo location longer than its column, and a fractional or oversized section index. Voices for a provider id that is not a number answer 404. Match the reader's blocks in chapter text: tag names now match whole, so <picture>, <param>, <link>, <track> and SVG shapes no longer open blocks foliate does not count. The chapter href is percent-decoded, and cached text is keyed by the file's modified time. Build a narration playlist once per copy of the file instead of on every overlay audio request, answer an unsatisfiable range with 416 and ignore an unreadable Range header. The iOS app now plays online read-along audio from this route in 512 KB ranges, so it is asked several times per clip. Give plain synthesis the captioned path's 120 s limit beside the client's own cancel signal, and voice listing and connection tests a 30 s one.
BookOrbit - Server
NestJS 12 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:6262/api/docs
- OpenAPI JSON: http://localhost:6262/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.