How Manual Collections Work in BookOrbit: A Complete Technical Guide

BookOrbit implements manual collections as a self-contained NestJS module that enforces ownership-based permissions through a layered architecture, allowing users to create private or public book groupings with full CRUD, reordering, and bulk management capabilities.

BookOrbit is an open-source book management platform that enables users to organize their libraries using manual collections—personal, user-curated groupings of books that can be edited, reordered, and synced to devices. The implementation follows a clean architecture pattern with strict separation between the data layer, business logic, and API presentation, utilizing Drizzle ORM for database operations and structured logging for auditability.

Server-Side Architecture for Manual Collections

The backend implementation resides in server/src/modules/collection/ and follows a layered architecture pattern with four distinct tiers.

Database Schema and Relationships

The schema definition in server/src/db/schema/collections.ts establishes the foundational data structure. The collections table stores metadata including name, icon, description, and the boolean isPublic flag (defaulting to false). A unique composite index enforces one-name-per-user constraints, while separate indexes optimize queries for public versus private visibility.

The many-to-many relationship between collections and books is managed through a collection_books join table, which includes foreign keys to both entities plus a display_order column for manual sorting.

Repository Layer with Drizzle ORM

collection.repository.ts encapsulates all low-level database operations using Drizzle ORM. Key responsibilities include:

  • Basic CRUD operations for collection records
  • Membership queries that respect library access scopes via buildReadableMembershipWhere
  • Batch updates for reordering operations
  • Cascade deletions for collection cleanup

The repository abstracts SQL complexity from the service layer, exposing methods like findByIdWithBookCount that hydrate collections with aggregated metadata.

Service Layer Business Logic

collection.service.ts contains the core business logic and permission enforcement. The service implements two critical private methods:

  • assertReadAccess – Validates that the requesting user can view the collection (owner, super-user, or public collection)
  • assertWriteAccess – Restricts modifications to owners and super-users only

Both methods throw ForbiddenException with the constant message COLLECTION_ACCESS_DENIED_MESSAGE when checks fail.

The service handles complex operations including create, update, remove, reorder, addBooks, removeBooks, and buildBooksWhere. When adding books, it resolves bulk selections via BookService.resolveSelectionToIds, supporting both explicit ID lists and query-based filters.

Controller and DTO Validation

collection.controller.ts defines HTTP endpoints under /api/v1/collections, applying the @Auditable decorator to emit audit events (AuditAction.CollectionCreate, AuditAction.CollectionUpdate, etc.).

Data validation occurs through DTOs defined in create-collection.dto.ts and collection-books.dto.ts. The bulk selection DTO inherits from the generic BulkSelectionDto, enabling flexible book selection patterns through the API.

Permission Model and Access Control

Manual collections implement a binary visibility model enforced at the service layer.

Read Access – Collections are readable if isPublic is true, or if the requesting user owns the collection. Super-users bypass all restrictions.

Write Access – Only the original owner or super-users can modify, reorder, or delete collections. This prevents unauthorized edits to shared public collections.

The permission checks run before any database mutations, ensuring consistent security across all operations.

Managing the Collection Lifecycle

The API supports a complete lifecycle for manual collections with seven primary operations:

  1. Create – POST /api/v1/collections accepts CreateCollectionDto to initialize a new grouping
  2. Read – GET /api/v1/collections/:id hydrates the collection with ownership flags and book counts
  3. Update – PATCH /api/v1/collections/:id partially modifies metadata fields
  4. Delete – DELETE /api/v1/collections/:id performs cascade deletion of collection_books rows
  5. Reorder – POST /api/v1/collections/reorder batch updates displayOrder values
  6. Book Management – POST and DELETE endpoints at /api/v1/collections/:id/books handle membership changes
  7. Query Books – GET /api/v1/collections/:id/books supports pagination, series collapsing, and full-text search via the q parameter

Adding Books via Explicit IDs

curl -X POST https://api.bookorbit.example/api/v1/collections/42/books \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"bookIds":[101,102,103]}'

Adding Books via Query Filters

Users can bulk-add books using filter criteria rather than explicit IDs:

curl -X POST https://api.bookorbit.example/api/v1/collections/42/books \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{
        "query": {
          "filter": {"tags":["fiction"]},
          "pagination":{"page":0,"size":50}
        }
      }'

The service resolves these queries to concrete IDs before inserting into collection_books.

Reordering Collections

Manual ordering is preserved through the displayOrder column:

curl -X POST https://api.bookorbit.example/api/v1/collections/reorder \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{
        "order": [
          {"id": 42, "displayOrder": 1},
          {"id": 17, "displayOrder": 2},
          {"id": 5,  "displayOrder": 3}
        ]
      }'

Client-Side Ownership Detection

While the server enforces all permissions, the client optimizes UI rendering through the canMutateCollection helper in client/src/features/collection/lib/collection-access.ts.

export function canMutateCollection(collection: Collection | null | undefined): boolean {
  return collection?.isOwner === true;
}

This utility enables conditional rendering of edit and delete buttons without additional API calls.

Structured Logging and Audit Trails

All service methods emit structured logs following the pattern [event] [phase]. For example, adding books generates:

this.logger.log(
  `[collection.add_books] [start] collectionId=${id} userId=${user.id} ` +
  `selectionMode=${this.getSelectionMode(dto)} requestedCount=${this.getRequestedCount(dto)} - add books started`,
);

Failure logs sanitize sensitive values via sanitizeLogValue. Combined with the @Auditable decorator on controller methods, this provides complete traceability for administrative monitoring.

Summary

  • Manual collections in BookOrbit are implemented as a modular NestJS feature with clear separation between schema, repository, service, and controller layers
  • Permission enforcement occurs in collection.service.ts through assertReadAccess and assertWriteAccess, distinguishing between public visibility and owner-only modifications
  • Bulk operations support both explicit ID lists and query-based selection, resolved through BookService.resolveSelectionToIds before database insertion
  • Ordering is maintained via the displayOrder column in the collection_books join table, updated through batch operations
  • Auditability is guaranteed through structured logging conventions and the @Auditable decorator on all mutation endpoints

Frequently Asked Questions

What is the difference between public and private manual collections in BookOrbit?

Public collections have isPublic set to true in the database schema, making them visible to all users regardless of ownership, while private collections are restricted to their creator and super-users. The collections_public_display_name_idx index optimizes query performance for public collections, and the assertReadAccess method enforces these visibility rules at the service layer.

How does BookOrbit prevent unauthorized users from editing manual collections?

The service layer implements assertWriteAccess to verify that the requesting user is either the collection owner or a super-user before allowing any modifications. If this check fails, the system throws a ForbiddenException with the message COLLECTION_ACCESS_DENIED_MESSAGE, preventing both API and database-level unauthorized changes.

Can users add books to a collection using search filters instead of selecting individual titles?

Yes, the CollectionBooksDto supports a query parameter that accepts filter criteria, pagination settings, and search terms. When provided, collection.service.ts delegates to BookService.resolveSelectionToIds to convert the query into a concrete list of book IDs before inserting them into the collection_books join table, enabling bulk operations based on tags, series, or full-text search.

What happens to the books when a manual collection is deleted?

Deleting a collection triggers a cascade delete operation in the repository layer that removes all associated rows from the collection_books join table while leaving the actual book records intact in the library. This ensures referential integrity without affecting the user's underlying book inventory.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →