* feat(types): add readwise_sync permission * feat(types): add readwise settings types * feat(db): add readwise_user_settings table * feat(readwise): add constants * feat(readwise): add throttle queue * feat(readwise): add REST client * feat(readwise): add repository * feat(readwise): add settings service * feat(readwise): add sync service with watermark * refactor(readwise): drop unused logger in settings service * feat(readwise): add debounced scheduler * feat(readwise): add event listener * feat(readwise): add controller and dtos * feat(readwise): wire module into app * test(readwise): add readwise_sync to authorization matrix * feat(annotation): emit annotation.created on device sync ingest * feat(client): add readwise api client * feat(client): add useReadwiseSettings composable * feat(client): add Readwise settings page * feat(client): add Readwise settings route * feat(client): add Readwise to settings nav * fix(readwise): avoid typeof this in settings type annotation Production build (nest build → tsc via tsconfig.build.json) rejects `Parameters<typeof this.repo.upsertSettings>[1]` with TS2304. Reference the repository class type directly so the prod image compiles. * fix(client): add Readwise sync to admin permission list The admin UserFormDrawer renders a hardcoded permission list, not an iteration of PERMISSION_LABELS, so readwise_sync was ungrantable via UI. Add it to the Devices & Access group and the Standard preset, mirroring Hardcover sync. * feat(readwise): send book cover to Readwise via OpenLibrary ISBN Populate the Readwise highlight image_url with the OpenLibrary cover URL built from the book's isbn13 (falling back to isbn10). Uses ?default=false so a missing cover yields 404 (no image) rather than a blank placeholder. Books without an ISBN send no image_url. BookOrbit's own cover endpoint is auth-gated and stores no public source URL, so a public third-party cover URL is the zero-infra way to give Readwise an image. * test(readwise): add type parameters to vi.fn mocks oxlint (vitest/require-mock-type-parameters) fails on untyped vi.fn() in the settings page test; the pre-commit hook only runs eslint, so this surfaced under full lint:check. * docs(readme): add Readwise Sync to features list * fix(readwise): surface settings fetch errors in the UI The settings page destructured error from useReadwiseSettings but never rendered it. A failed fetch on mount left the user with an empty default form and no indication that loading failed. Render an error banner when error is set, and cover it with component tests. * test(readwise): cover settings page token test, save, and visibility toggle Adds component tests for the Test-connection flow (empty/valid/invalid token), Save (success clears the field, failure surfaces an error toast), and the show/hide token toggle — the interactive handlers Codecov flagged as uncovered in ReadwiseSettings.vue. * test(readwise): match settings buttons by exact label in test helper Use an exact text match instead of substring matching in the testButton helper so it can never return the wrong button if labels overlap. * fix(readwise): harden highlight sync --------- Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com> Co-authored-by: Oscar Nogueira Neto <oscarnogueira@gmail.com>
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.