Skip to main content
The reading progress router tracks how far users have read in each book and their reading status.

Save progress

Update reading progress for a book.
await client.readingProgress.saveProgress({
  bookUuid: "book-uuid",
  ttuBookId: 123,
  exploredCharCount: 5000,
  bookCharCount: 100000,
  readingTimeSeconds: 1800,
  status: "reading",
});

Input

bookUuid
string
required
Book UUID
ttuBookId
number
Internal TTU book ID (for epub.js integration)
exploredCharCount
number
Number of characters read so far
bookCharCount
number
Total characters in the book
readingTimeSeconds
number
Total reading time in seconds
status
enum
Reading status: unread, reading, or completed

Response

id
number
Reading progress record ID
userId
string
User ID
bookUuid
string
Book UUID
ttuBookId
number | null
TTU book ID
exploredCharCount
number | null
Characters read
bookCharCount
number | null
Total characters
readingTimeSeconds
number | null
Reading time in seconds
status
string | null
Current reading status
lastReadAt
string
ISO timestamp of last read session
createdAt
string
ISO timestamp when record was created

Get progress

Retrieve reading progress for a specific book.
const progress = await client.readingProgress.getProgress({
  bookUuid: "book-uuid",
});

Input

bookUuid
string
required
Book UUID

Response

Reading progress object, or null if no progress exists for this book.

List in-progress books

Get all books the user is currently reading.
const books = await client.readingProgress.listInProgress({
  limit: 20,
});

Input

limit
number
default:20
Maximum number of books to return (1-50)

Response

Array of objects containing:
book
object
Complete book object (same structure as books.getBookWithMetadata)
progress
object
Reading progress object for this book

Reading statuses

The reading status enum is defined in packages/api/src/constants.ts:
export const READING_STATUSES = {
  UNREAD: "unread",
  READING: "reading",
  COMPLETED: "completed",
} as const;

Input schema

The progress input schema is defined in packages/api/src/routers/reading-progress/reading-progress.model.ts:
export const SaveProgressInput = z.object({
  bookUuid: z.string(),
  ttuBookId: z.number().int().optional(),
  exploredCharCount: z.number().int().optional(),
  bookCharCount: z.number().int().optional(),
  readingTimeSeconds: z.number().int().optional(),
  status: z
    .enum([
      READING_STATUSES.UNREAD,
      READING_STATUSES.READING,
      READING_STATUSES.COMPLETED,
    ])
    .optional(),
});

Example: Reading progress tracker

import { useEffect } from "react";
import { orpc } from "@/utils/orpc";

function BookReader({ bookUuid }: { bookUuid: string }) {
  const { data: progress } = orpc.readingProgress.getProgress.useQuery({
    bookUuid,
  });
  
  const saveProgress = orpc.readingProgress.saveProgress.useMutation();
  
  // Auto-save progress every 30 seconds
  useEffect(() => {
    const interval = setInterval(() => {
      const exploredChars = getExploredCharCount(); // Your implementation
      const totalChars = getTotalCharCount();
      const readingTime = getReadingTimeSeconds();
      
      saveProgress.mutate({
        bookUuid,
        exploredCharCount: exploredChars,
        bookCharCount: totalChars,
        readingTimeSeconds: readingTime,
        status: "reading",
      });
    }, 30000);
    
    return () => clearInterval(interval);
  }, [bookUuid]);
  
  const percentComplete = progress
    ? (progress.exploredCharCount / progress.bookCharCount) * 100
    : 0;
  
  return (
    <div>
      <div>Progress: {percentComplete.toFixed(1)}%</div>
      <div>Reading time: {Math.floor(progress?.readingTimeSeconds / 60)} minutes</div>
      {/* Your reader component */}
    </div>
  );
}

Example: Continue reading section

import { orpc } from "@/utils/orpc";

function ContinueReading() {
  const { data: books } = orpc.readingProgress.listInProgress.useQuery({
    limit: 10,
  });
  
  return (
    <div>
      <h2>Continue reading</h2>
      {books?.map(({ book, progress }) => {
        const percent = (progress.exploredCharCount / progress.bookCharCount) * 100;
        
        return (
          <div key={book.uuid}>
            <img src={book.cover} alt={book.title} />
            <h3>{book.title}</h3>
            <div>
              <progress value={percent} max={100} />
              <span>{percent.toFixed(0)}% complete</span>
            </div>
          </div>
        );
      })}
    </div>
  );
}

Build docs developers (and LLMs) love