Last Updated: November 24, 2025 Status: Current - SQLite + Drizzle ORM + Repository Pattern Audience: AI agents, developers
Tome is a self-hosted reading progress tracker that integrates with Calibre digital libraries. It enables users to track reading progress, manage reading status (to-read, reading, read), maintain reading streaks, and view comprehensive reading statistics.
- Read-only access to Calibre's SQLite database (metadata.db) for most operations
- Limited write access: Only for ratings and tag management (see Write Safety below)
- Automatic sync: File watcher monitors Calibre database for changes and syncs within 2 seconds
- Rating sync: Bidirectional sync with Calibre's rating system (Tome 1-5 stars ↔ Calibre 2/4/6/8/10)
- Tag sync: Tome uses Calibre tags for shelving (writing tags back to Calibre)
- No data export: Calibre remains the source of truth for book metadata
Tome implements multiple safety mechanisms when writing to the Calibre database:
Approved Write Operations:
updateCalibreRating(calibreId, stars)- Updatesratingsandbooks_ratings_linktablesupdateCalibreTags(calibreId, tags)- Updatestagsandbooks_tags_linktablesbatchUpdateCalibreTags(operations)- Bulk tag updates for shelving
Safety Mechanisms:
- Retry Logic (
lib/calibre-watcher.ts) - Automatically retries sync on database lock errors- 3 retries with exponential backoff (1s, 2s, 3s)
- Detects
SQLITE_BUSY/SQLITE_LOCKEDerrors - Non-lock errors fail immediately
- Watcher Suspension - Pauses file watcher during writes to prevent re-syncing self-inflicted changes
suspend()- Stops watchingresume()- Resumes immediatelyresumeWithIgnorePeriod(ms)- Resumes after ignore period
- Debouncing - 2-second debounce on file changes to prevent sync storms
- Single-Instance Guard -
isSyncingflag prevents concurrent syncs - Enhanced Error Messages - Clear, actionable lock error messages with operation context
User Requirements:
- Close Calibre before tag operations (adding/removing books from shelves)
- Rating updates and auto-sync work fine with Calibre open (retry logic handles transient locks)
For details: See Calibre Database Safety Guide
- Setup: Point Tome to Calibre library → Initial sync imports all books
- Reading Progress: Log pages/percentage as reading → Progress tracked per session
- Status Management: Change book status (to-read → reading → read)
- Re-reading: Complete a book, then start a new session for same book with full history preserved
- Rating: Rate books on finish → Syncs to Calibre automatically
- Streaks: Daily reading activity tracked → Current and longest streak calculated
- Framework: Next.js 14 (App Router, React 18)
- Language: TypeScript
- Styling: Tailwind CSS
- Icons: Lucide React
- Charts: Recharts
- Client State: React hooks + custom hooks (useLibraryData)
- Client Service Layer: LibraryService (singleton with caching)
- Runtime: Bun (production) / Node.js (development)
- API: Next.js API Routes (REST)
- Services: Three-tier service layer (BookService, SessionService, ProgressService)
- Data Access: Repository Pattern (5 repositories + BaseRepository)
- Tome Database: SQLite + Drizzle ORM (tracking data: books, sessions, progress, streaks)
- Location:
data/tome.db - Driver:
bun:sqlite(Bun) /better-sqlite3(Node.js) - Factory Pattern:
lib/db/factory.tshandles runtime detection
- Location:
- Calibre Database: SQLite (metadata.db from Calibre library)
- Read operations:
lib/db/calibre.ts(read-only connection) - Write operations:
lib/db/calibre-write.ts(ratings and tags only) - Safety: See Calibre Write Safety above
- Read operations:
- Containerization: Docker + Docker Compose
- Web Server: Built-in Next.js server (port 3000)
- Volume Management: Single persistent volume for
data/tome.db - Auto-migration: Migrations run on container startup with safety features
drizzle-orm: SQLite ORM with SQL-like syntaxzod: Validationpino: Loggingchokidar: File system watching (Calibre sync)
Location: lib/db/schema/ (Drizzle schemas)
Purpose: Stores metadata of books synced from Calibre
Key Fields:
calibreId(unique) - Calibre database IDtitle,authors(JSON),tags(JSON)totalPages,rating(1-5 stars)orphaned- Marks books removed from Calibre- Timestamps:
lastSynced,createdAt,updatedAt
Repository: bookRepository | Schema: lib/db/schema/books.ts
Purpose: Tracks reading sessions per book (supports re-reading)
Key Fields:
bookId(FK → books.id, CASCADE DELETE)sessionNumber- Enables re-reading (1, 2, 3...)isActive- Only one active session per bookstatus- Enum: to-read, read-next, reading, readreadNextOrder- Custom sort order for read-next queue (0, 1, 2...)startedDate,completedDate(TEXT: YYYY-MM-DD) - Calendar days, not timestampsreview
Date Storage: Uses YYYY-MM-DD strings (not timestamps) for semantic correctness. See ADR-014: Date String Storage
Read-Next Ordering: Auto-compaction on status changes (see Pattern 12). Partial index on (readNextOrder, id) WHERE status='read-next'.
Indexes: Unique on (bookId, sessionNumber); Partial unique on (bookId) WHERE isActive=1; Partial on (readNextOrder, id) WHERE status='read-next'
Repository: sessionRepository | Schema: lib/db/schema/reading-sessions.ts
Purpose: Tracks individual reading progress entries per session
Key Fields:
bookId,sessionId(FK with CASCADE DELETE)currentPage,currentPercentage(0-100)pagesRead- Delta from previous entryprogressDate(TEXT: YYYY-MM-DD) - Calendar day, not timestampnotes
Date Storage: Uses YYYY-MM-DD strings (not timestamps) for semantic correctness. See ADR-014: Date String Storage
Indexes: (bookId, progressDate DESC), (sessionId, progressDate DESC)
Repository: progressRepository | Schema: lib/db/schema/progress-logs.ts
Purpose: Tracks reading consistency streaks with timezone-aware auto-reset
Key Fields:
currentStreak,longestStreak- Consecutive days meeting thresholdlastActivityDate,streakStartDate- Dates in UTC (interpreted in user timezone)totalDaysActive- Lifetime count of days meeting thresholddailyThreshold- Pages required per day (default: 1, configurable 1-9999)userTimezone- IANA timezone identifier (default: 'America/New_York')lastCheckedDate- Idempotency flag for daily auto-reset checks
Pattern: Singleton (one record per user)
Auto-Reset: Check-on-read with idempotency (FR-005 from spec 001)
Timezone Support: Per-user timezone with auto-detection (FR-011 from spec 001)
Repository: streakRepository | Schema: lib/db/schema/streaks.ts
Service: streakService (lib/services/streak.service.ts) - Single source of truth
Book (synced from Calibre)
├── ReadingSession (1:many per book - supports re-reading)
│ ├── sessionNumber (1, 2, 3...)
│ ├── isActive (only one active session per book)
│ ├── status, dates, review
│ └── ProgressLog (1:many per session)
│ └── sessionId links progress to specific reading session
└── Streak (global - aggregated from all ProgressLog entries across all sessions)
└── Calculated streak metrics (all sessions count toward streaks)
Key Relationships:
- One Book can have multiple ReadingSessions (for re-reading)
- Foreign key: readingSessions.bookId → books.id (CASCADE DELETE)
- Only one ReadingSession per book can be active (isActive=1)
- Foreign key: progressLogs.bookId → books.id (CASCADE DELETE)
- Foreign key: progressLogs.sessionId → readingSessions.id (CASCADE DELETE)
- Each ProgressLog entry links to a specific ReadingSession
- Streaks are calculated from ALL progress logs across ALL sessions and books
Rule: All Tome database access goes through repositories.
Repositories: bookRepository, sessionRepository, progressRepository, streakRepository
Details: See docs/REPOSITORY_PATTERN_GUIDE.md
Read Operations (lib/db/calibre.ts):
getAllBooks(): Fetch all books with metadatagetBookById(id): Get specific book detailssearchBooks(query): Full-text search by title/author- Handles many-to-many: books → authors, tags via junction tables
Write Operations (lib/db/calibre-write.ts) - RATINGS ONLY:
updateCalibreRating(calibreId, stars): Update book rating- Converts Tome 1-5 stars → Calibre 2/4/6/8/10 scale
- Manages ratings and books_ratings_link tables
- Best-effort (continues if Calibre unavailable)
All Tome database access goes through repositories:
- BaseRepository: Generic CRUD (findById, create, update, delete, find, count, exists, etc.)
- BookRepository: findByCalibreId, findWithFilters (complex filtering), getAllTags, markAsOrphaned
- SessionRepository: findActiveByBookId, getNextSessionNumber, deactivateOtherSessions, getNextReadNextOrder, reorderReadNextBooks, reindexReadNextOrders
- ProgressRepository: findBySessionId, findLatestByBookId, getUniqueDatesWithProgress
- StreakRepository: getActiveStreak, upsertStreak
Key Pattern: Repository methods coordinate with services; services orchestrate repositories.
Routes (HTTP layer - 30-50 lines thin orchestrators)
↓
Services (Business logic - 3 services, 735 lines)
├─ BookService: Book retrieval, filtering, metadata updates, rating sync
├─ SessionService: Session lifecycle, status transitions, re-reading, streaks
└─ ProgressService: Progress logging, validation, calculations, auto-completion
↓
Repositories (Data access - 5 specialized repos)
├─ BookRepository
├─ SessionRepository
├─ ProgressRepository
├─ StreakRepository
└─ External access: Calibre read/write
↓
Database (SQLite + Drizzle ORM)
Page-level patterns:
-
Dashboard (Server Component)
- Fetches stats and streak server-side
- Displays: Currently reading, key metrics, streaks
-
Library (Client Component)
- Architecture: Page → Hook → Service → API → DB
- Page handles URL params and orchestration
useLibraryDatahook manages filter state, pagination, loadingLibraryServicesingleton provides caching and API abstraction- Infinite scroll pagination with hasMore calculation
- Features: Search, status filter, tag filter, sorting, sync trigger
-
Book Detail (Client Component)
- Architecture: Page → Custom Hooks → Presentation Components → API
useBookDetail: Data fetching, image errorsuseBookStatus: Status transitions with validation, confirmations, re-readinguseBookProgress: Progress logging, editing, deletion, temporal validationuseBookRating: Rating modal state, updates- Components: BookHeader, BookMetadata, BookProgress, ProgressHistory, SessionDetails
- Refactored from 1,223 lines to ~250 lines orchestrator + 5 focused components
-
Statistics (Server Component)
- Comprehensive stats: Streaks, total/year/month books, pages, velocity
-
Settings (Client Component)
- Calibre path display, sync status, manual sync button
GET /api/books
- Fetch paginated book list
- Query params:
status,search,tags,rating,limit,skip,showOrphaned,sortBy - Returns: books array with active session status and book rating, total count
- Joins with active ReadingSession (isActive=true) for user data
PATCH /api/books/:id
- Update book (totalPages)
- Body:
{ totalPages } - Returns: updated book
GET /api/books/:id
- Fetch single book with full details
- Returns: Book + active ReadingSession + latest ProgressLog for active session
- Includes: book metadata, current session status, current progress
GET /api/books/:id/status
- Fetch active reading session for a book
- Returns: active ReadingSession (isActive=true) or null
POST /api/books/:id/status
- Update active reading session status
- Body:
{ status, rating?, review?, startedDate?, completedDate? } - Creates new session if none exists (auto-increments sessionNumber)
- Auto-sets dates when status changes:
- "reading" → sets startedDate
- "read" → sets completedDate
- Note:
ratingupdates the book's rating (books.rating), not stored on session - Returns: updated ReadingSession
GET /api/books/:id/sessions
- Fetch all reading sessions for a book (supports re-reading history)
- Returns: array of ReadingSession objects (sorted by sessionNumber desc)
- Each session includes:
- Session metadata (sessionNumber, status, dates, review)
- Progress summary (totalEntries, totalPagesRead, latestProgress, date range)
POST /api/books/:id/reread
- Start a new reading session for a book (re-reading)
- Requirements: active session must have status="read"
- Process:
- Archives current active session (sets isActive=false)
- Creates new session (sessionNumber++, status="reading", isActive=true)
- Rebuilds streak from all progress logs
- Returns: new session + archived session info
GET /api/sessions/read-next
- Fetch all read-next books sorted by custom order
- Query params:
search(optional - filters by title/author) - Returns: array of ReadingSession with status='read-next', sorted by readNextOrder ASC
- Used by:
/read-nextpage
PUT /api/sessions/read-next/reorder
- Batch reorder read-next books (drag-and-drop)
- Body:
{ updates: Array<{ id: number, readNextOrder: number }> } - Validation: Zod schema (id=number, readNextOrder>=0)
- Returns:
{ success: true } - Transaction safety: All updates in single transaction
GET /api/books/:id/progress
- Fetch progress logs for a book's active session (or specific session with
?sessionId=...) - Query params:
sessionId(optional - defaults to active session) - Sorted by progressDate descending
- Returns: array of ProgressLog entries for the session
POST /api/books/:id/progress
- Log reading progress for active session
- Body:
{ currentPage?, currentPercentage?, notes? } - Requires active ReadingSession to exist
- Calculates:
- Final percentage from pages (if not provided)
- Final pages from percentage (if not provided)
- pagesRead as delta from last entry in this session
- Auto-updates active session status to "read" if 100% reached
- Links progress entry to active session via sessionId
- Triggers streak update (counts across all sessions)
- Returns: created ProgressLog
PATCH /api/books/:id/progress/:progressId
- Edit existing progress entry
- Body:
{ currentPage?, currentPercentage?, notes? } - Returns: updated ProgressLog
DELETE /api/books/:id/progress/:progressId
- Delete progress entry
- Returns: success message
GET /api/stats/overview
- Global reading statistics
- Returns:
{ booksRead: { total, thisYear, thisMonth }, currentlyReading: number, pagesRead: { total, thisYear, thisMonth, today }, avgPagesPerDay: number (last 30 days) }
GET /api/stats/activity
- Activity calendar and monthly breakdown
- Query params:
year,month(optional) - Returns:
{ calendar: [{ date: "YYYY-MM-DD", pagesRead }], monthly: [{ month, year, pagesRead }] }
GET /api/streak
- Get streak with hours remaining today
- Auto-calls
checkAndResetStreakIfNeeded()before returning data - Returns:
Streak+hoursRemainingToday - Auto-creates if doesn't exist
GET /api/streaks
- Get basic streak data (no computed fields)
- Auto-calls
checkAndResetStreakIfNeeded()before returning data - Returns: full
Streakobject - Auto-creates if doesn't exist
PATCH /api/streak/threshold
- Update daily page threshold (1-9999 pages)
- Body:
{ threshold: number } - Returns: updated
Streakobject
POST /api/streak/timezone
- Auto-detect and set user's timezone (only if using default)
- Body:
{ timezone: string }(IANA identifier, e.g., "America/New_York") - Returns:
{ success: boolean, timezone: string, streakRebuilt: boolean } - Idempotent: Only updates if current timezone is default
PATCH /api/streak/timezone
- Manually change user's timezone
- Body:
{ timezone: string } - Triggers full streak rebuild with new timezone
- Returns:
{ success: boolean, timezone: string, streakRebuilt: true }
Streak Auto-Reset Logic:
- Pattern: Check-on-read with idempotency
- Trigger: Called before any streak data retrieval
- Check: Runs once per day (uses
lastCheckedDateflag) - Condition: Resets
currentStreakto 0 if >1 day since last activity - Timezone: Uses user's configured timezone for day boundaries
GET /api/calibre/sync
- Manually trigger sync with Calibre
- Prevents concurrent syncs
- Returns:
{ success: boolean, message: string, syncedCount: number, updatedCount: number, totalBooks: number, lastSync: Date }
GET /api/calibre/status
- Get sync status without triggering sync
- Returns:
{ lastSync: Date | null, syncInProgress: boolean, autoSyncEnabled: boolean }
GET /api/covers/:path*
- Stream book cover images from Calibre library
- Dynamic path based on book location in Calibre
- Security: validates path stays within library directory
- Caching: 1-year immutable cache headers
- Returns: image binary with appropriate Content-Type
Progress Tracking:
- User logs progress → API validates and calculates delta
- Creates ProgressLog linked to active session
- Updates streak if consecutive day
- Auto-marks session as "read" at 100%
Calibre Sync:
- Automatic: File watcher detects change → 2s debounce → sync via repository
- Manual: User triggers → sync via repository → update UI
See implementation details in lib/sync-service.ts and lib/services/streak.service.ts
- Each book can have multiple
ReadingSessionrecords (sessionNumber 1, 2, 3...) - Only one session active per book (isActive=true)
- Previous sessions archived (isActive=false) with full history preserved
- Progress isolated per session (sessionId foreign key)
- Streaks accumulate across all sessions
User Flow:
- Mark book as "reading" → Creates new session
- Log progress → Links to active session
- Mark as "read" → Archives session, triggers streak update
- Click "Start Re-reading" → Creates session #2, archives previous
- View "Reading History" → All archived sessions with summaries
- Supports two input modes: Pages or percentage
- Temporal validation: Enforces timeline consistency (no backward progress without backdating)
- Auto-completion: 100% progress auto-marks session as "read"
- Calculations: Automatic page/percentage conversion, pagesRead delta
- Notes: User notes attached to each entry
- Backdating: Can add historical entries for book club scenarios
Purpose: Dedicated page for managing "read-next" status books with custom ordering
Features:
- Custom order persistence: Users drag-and-drop to reorder books
- Auto-compaction: Gaps eliminated automatically when books leave queue
- Default behavior: New books append to end (natural queue behavior)
- Search/filter: In-page search by title/author (300ms debounce)
- Bulk actions: Remove multiple books from read-next (→ to-read status)
- Navigation: Dedicated
/read-nextpage accessible from main nav and dashboard
Implementation (Pattern 12):
readNextOrdercolumn onreading_sessionstable (default 0)- Partial index:
(readNextOrder, id) WHERE status='read-next' - Auto-assignment: Sequential order when entering read-next status
- Auto-compact trigger: Service layer on all read-next transitions
- API: Batch reorder endpoint for drag-and-drop efficiency
User Flow:
- User changes book status to "read-next" → Auto-assigned next order (e.g., 3)
- User drags books on
/read-nextpage → Batch reorder API call - User changes book to "reading" → Order reset to 0, remaining books renumbered (0, 1, 2...)
- Dashboard card "View all" links to
/read-next(not filtered library)
See: Pattern 12 in .specify/memory/patterns.md for complete implementation details
Overview: Timezone-aware streak tracking with configurable thresholds and automatic reset detection.
Core Metrics:
currentStreak: Consecutive days meeting daily threshold (timezone-aware)longestStreak: All-time best streaktotalDaysActive: Lifetime count of days meeting thresholddailyThreshold: Pages required per day (default: 1, range: 1-9999)userTimezone: IANA timezone identifier (default: 'America/New_York')
Key Features:
-
Per-User Timezone Support (FR-011):
- Auto-detection: Frontend detects device timezone on first visit
- Manual override: Timezone selector in Settings
- Day boundaries: All calculations use user's local midnight, not UTC
- DST handling: Automatic handling of daylight saving transitions
-
Configurable Thresholds (FR-012):
- Users set personal daily goals (1-9999 pages)
- Validation: Must be positive integer in range
- Immediate application: New threshold applies to current day
- Historical preservation: Past days evaluated with their original threshold
-
Auto-Reset Detection (FR-005):
- Pattern: Check-on-read with idempotency (no cron jobs)
- Trigger: Called before any streak data retrieval
- Check: Runs once per day (uses
lastCheckedDateflag) - Condition: Resets to 0 if >1 day since last activity
- Timezone-aware: Uses user's configured timezone for gap detection
-
Timezone-Aware Calculation:
- All progress aggregated by LOCAL calendar day (not UTC day)
- Uses
date-fns-tzfor timezone conversions - Pattern: Store UTC, calculate in user timezone
- Example: 8 AM EST progress counts toward "today" (not "yesterday UTC")
Architecture:
User Logs Progress → API → checkAndResetStreakIfNeeded() → updateStreaks()
↓ (idempotent daily check)
Reset if >1 day gap → Aggregate by local day → Update streak
Timezone Pattern:
// Convert UTC to user timezone
const todayInUserTz = startOfDay(toZonedTime(new Date(), userTimezone));
// Perform calculations in user timezone
const daysSinceLastActivity = differenceInDays(todayInUserTz, lastActivity);
// Convert back to UTC for storage
const todayUtc = fromZonedTime(todayInUserTz, userTimezone);Streak Rebuild:
- Triggered by: Progress logging, re-reading, timezone changes
- Process:
- Get all progress logs across all sessions
- Group by local date (YYYY-MM-DD in user's timezone)
- Filter days meeting threshold
- Calculate consecutive sequences
- Check for broken streaks (>1 day gap from today)
- Timezone-aware: All date comparisons use user's timezone
Database Dependencies:
- Calculated from ALL progress logs across ALL sessions and books
- Single
streaksrecord per user (singleton pattern) - Timezone metadata:
userTimezone,lastCheckedDate - Progress date storage: UTC epoch, interpreted in user timezone
Frontend Integration:
TimezoneDetector: Auto-detects and sets timezone on first visitStreakSettings: Manual timezone picker with common timezonesStreakDisplay: Shows current/longest streak with visual indicators- Goal completion: Dynamic UI based on
todayPagesReadvsthreshold
Edge Cases Tested:
- DST transitions (Spring Forward / Fall Back)
- Timezone changes (user moves or changes settings)
- Cross-timezone midnight (11:59 PM → 12:01 AM)
- UTC vs local day boundaries
- Multi-log aggregation within same day
Implementation:
- Service:
lib/services/streak.service.ts(single source of truth, repository pattern) - Repository:
lib/repositories/streak.repository.ts - API:
app/api/streak/(timezone endpoints, threshold updates)
Specification: See specs/001-reading-streak-tracking/ for full requirements and acceptance criteria
- Stored: books.rating (1-5 stars only, NULL for unrated)
- Not stored per session: reading_sessions.review (personal notes, no rating per session)
- Calibre sync:
- Write: Tome update → Calibre (best effort, continues if unavailable)
- Read: Calibre → Tome on sync
- Scale: 1-5 ↔ 2/4/6/8/10
- Filtering: Library supports exact/range/unrated filters with URL persistence
- Sorting: By rating high-to-low or low-to-high (unrated last)
- Mechanism: File watcher (
lib/calibre-watcher.ts) + Node.js instrumentation hook - Trigger: Calibre metadata.db file modification detected
- Debounce: 2-second delay prevents thrashing on rapid changes
- Works in: Both dev (Node.js + better-sqlite3) and production (Bun + bun:sqlite)
- Concurrency: isSyncing flag prevents concurrent syncs
- Process:
- Connect to both Tome and Calibre SQLite databases
- Fetch all books from Calibre via getAllBooks()
- For each book: Create or update in Tome database
- Mark removed books as orphaned
- Update sync timestamp
Critical Patterns (see .specify/memory/patterns.md for code examples):
- Database Factory - Automatic runtime detection (Bun/Node), never import drivers directly
- Repository Pattern (PRIMARY) - All database access through repositories, never bypass
- Service Layer - Thin routes (30-50 lines), business logic in services
- Test Isolation - Use
setDatabase(testDb)andresetDatabase(), no global mocks - Client Service Layer - Page → Hook → Service → API pattern for complex pages
Storage Format: All calendar day dates (progress dates, session dates) are stored as YYYY-MM-DD strings (TEXT columns) in the database, representing calendar days independent of timezone.
Why Strings? Calendar days are semantically different from timestamps. When a user logs "I read on January 8th," they mean a calendar day in their life, not a specific moment in time. Storing as strings ensures dates never shift when users change timezones.
Key Principle: What the user logs is what they see. A date logged as "2025-01-08" remains "2025-01-08" regardless of timezone changes.
See: ADR-014: Date String Storage for complete rationale and migration details.
Two Patterns for Creating Date Strings:
Use toDateString(date) from utils/dateHelpers.server.ts when:
- Converting Date objects for database queries
- Working with dates already in UTC
- Comparing calendar days at UTC midnight
import { toDateString } from "@/utils/dateHelpers.server";
// Example: Converting Date for database query
const dateStr = toDateString(new Date()); // "2025-01-10"
await progressRepository.findAfterDate(dateStr);Used in:
lib/repositories/progress.repository.ts(10 usages) - Date range querieslib/dashboard-service.ts(2 usages) - Year/month start calculations
Use formatInTimeZone() from date-fns-tz when:
- Getting "today" in user's timezone
- Converting dates for display or comparison in user's local context
- Timezone matters semantically (e.g., "today's progress")
import { formatInTimeZone } from 'date-fns-tz';
import { getCurrentUserTimezone } from '@/utils/dateHelpers.server';
// Example: Getting today in user's timezone
const userTimezone = await getCurrentUserTimezone();
const todayInUserTz = formatInTimeZone(new Date(), userTimezone, 'yyyy-MM-dd');Used in:
lib/services/progress.service.ts(1 usage) - Getting today's datelib/services/streak.service.ts(2 usages) - Date comparisons with timezone awareness
Rule of Thumb: If timezone matters semantically, use formatInTimeZone(). Otherwise, use toDateString().
Historical Note: Before v0.5.0, dates were stored as INTEGER timestamps and required a complex timezone conversion layer (242 lines). This was removed in favor of string storage for semantic correctness and simplicity. See migrations in scripts/migrations/migrate-*-dates-to-text.ts and ADR-014: Date String Storage.
Key Directories:
lib/- Business logic: db (schemas, factory), repositories, servicesapp/- Next.js App Router: pages and API routeshooks/- Custom React hooks for state managementcomponents/- Reusable React components__tests__/- Test suites (99+ tests)drizzle/- Database migrationsdocs/- Architecture docs and ADRs
Architecture Layers:
- Routes (
app/api/) - HTTP handlers - Services (
lib/services/) - Business logic - Repositories (
lib/repositories/) - Data access - Database (
lib/db/) - Drizzle schemas + factory pattern
For detailed code examples and implementation patterns, see:
.specify/memory/patterns.md- 10 production-tested patterns with complete codedocs/AI_CODING_PATTERNS.md- Critical patterns and code stylesdocs/REPOSITORY_PATTERN_GUIDE.md- Complete repository documentation with examples
- ADR-001: MongoDB → SQLite migration (completed Nov 19, 2025)
- ADR-002: Book rating system (completed Nov 20, 2025)
- ADR-003: Book detail page refactoring (in progress)
- ADR-004: Backend service layer (completed Nov 21, 2025)
- ADR-006: Timezone-aware date handling (superseded Jan 10, 2026)
- ADR-014: Date string storage for calendar days (completed Jan 10, 2026)
- AI_CODING_PATTERNS.md: Single source of truth for coding patterns
- REPOSITORY_PATTERN_GUIDE.md: Complete repository documentation
- patterns.md (
.specify/memory/): Extracted reusable implementation patterns
Need Tome database access?
└─ Use repositories (lib/repositories/)
Need Calibre read access?
└─ Use lib/db/calibre.ts functions
Need to write to Calibre?
└─ Use updateCalibreRating() ONLY
Adding business logic?
└─ Put in appropriate service
├─ BookService (book operations)
├─ SessionService (session lifecycle)
└─ ProgressService (progress tracking)
Testing database code?
└─ Use setDatabase(testDb) + resetDatabase()
Complex client-side filtering?
└─ Use Page → Hook → Service pattern
Need client-side caching?
└─ Use LibraryService singleton pattern
Unsure about a pattern?
└─ Check .specify/memory/patterns.md (with code examples)
For comprehensive details, see:
- Constitution:
.specify/memory/constitution.md - Patterns:
.specify/memory/patterns.md - Coding Standards:
docs/AI_CODING_PATTERNS.md - Repositories:
docs/REPOSITORY_PATTERN_GUIDE.md - ADRs:
docs/ADRs/ - Logging Guide:
docs/LOGGING_GUIDE.md