Skip to content

Database Schema ​

Auto-generated from services/api-hono/src/db/schema.ts. Do not edit manually. Run vp run -F @libris/docs generate:db -- --db to regenerate.

PostgreSQL with Drizzle ORM. Migrations auto-apply on startup via runMigrations() in services/api-hono/src/bootstrap.ts (the drizzle-orm/postgres-js migrator in src/db/migrate.ts); they run before the database pool is created and are skipped when NODE_ENV=test.

Enums ​

book_status ​

Values: inbox, review, organized

reading_status ​

Values: unread, reading, finished, paused

Tables ​

TableDescription
booksMain book records with status, metadata, and the full-text search vector
book_filesPhysical file storage per book (format, checksums, content hashes, paths)
book_metadata_candidatesMetadata candidates per book and source (file, hardcover) shown during review
reading_progressCurrent KoReader/KoSync reading position per document and device
reading_progress_historyAppend-only history of reading-progress snapshots
reading_aggregatePer-(user, book) reading lifecycle: effective status plus started/finished/paused dates and any manual override
service_credentialsPer-user Hardcover API token (sealed, reversible). username is a label, not an identity — it is not unique across users
kosync_credentialsPer-user KoSync login: the username KOReader sends plus a sha256 of the secret it puts on the wire
app_settingsGlobal key/value application settings (e.g. Hardcover metadata/sync toggles)
upload_registryChecksum → uploader (user) ownership mapping consumed by the book-detected worker
hardcover_sync_logPer-(user, book) Hardcover sync tracking and last status

books ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
statusenum(book_status)Noinbox
titletextYes—
authortextYes—
isbn_10textYes—
isbn_13textYes—
publishertextYes—
published_yearintegerYes—
languagetextYes—
descriptiontextYes—
cover_urltextYes—
cover_pathtextYes—
page_countintegerYes—
seriestextYes—
series_indexrealYes—
genrestext[]No[]
tagstext[]No[]
hardcover_book_idintegerYes—
hardcover_edition_idintegerYes—
created_bytextNo—FK → users.id (ON DELETE restrict)
possible_duplicate_ofuuidYes—
approved_attimestamptzYes—
created_attimestamptzNonow()
updated_attimestamptzNonow()
search_vectortsvectorYes—

Indexes: books_status_created_at_idx, books_isbn13_idx, books_search_vector_idx (GIN), books_title_trgm_idx (GIN), books_author_trgm_idx (GIN), books_series_idx, books_possible_duplicate_of_idx, books_created_by_idx, books_hardcover_book_id_idx, books_series_series_index_uniq (unique)

book_files ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
book_iduuidNo—FK → books.id (ON DELETE cascade)
formattextNo—
original_nametextNo—
storage_pathtextYes—
inbox_pathtextYes—
file_sizebigintNo0
checksumtextYes—
content_hashtextYes—
original_content_hashtextYes—
created_attimestamptzNonow()
updated_attimestamptzNonow()

Indexes: book_files_book_id_idx, book_files_checksum_idx, book_files_content_hash_idx, book_files_original_content_hash_idx

book_metadata_candidates ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
book_iduuidNo—FK → books.id (ON DELETE cascade)
sourcetextNo—
raw_responsejsonbYes—
normalizedjsonbNo{}
confidencenumeric(5,4)No0
selected_fieldstext[]No[]

Indexes: book_metadata_candidates_book_id_idx

reading_progress ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
book_iduuidYes—FK → books.id (ON DELETE set null)
user_idtextNo—FK → users.id (ON DELETE cascade)
documenttextNo—
devicetextNo—
device_idtextYes—
progresstextNo—
percentagenumeric(5,4)No0
timestampbigintNo0
raw_payloadjsonbYes—
updated_attimestamptzNonow()

Indexes: reading_progress_device_idx, reading_progress_book_id_idx, reading_progress_user_id_idx

reading_progress_history ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
book_iduuidYes—FK → books.id (ON DELETE set null)
user_idtextYes—FK → users.id (ON DELETE set null)
documenttextNo—
devicetextNo—
progresstextNo—
percentagenumeric(5,4)No0
timestampbigintNo0
created_attimestamptzNonow()

Indexes: reading_progress_history_document_created_at_idx, reading_progress_history_created_at_idx, reading_progress_history_book_id_idx, reading_progress_history_user_id_idx, reading_progress_history_user_stream_created_at_idx

reading_aggregate ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
user_idtextNo—FK → users.id (ON DELETE cascade)
book_iduuidYes—FK → books.id (ON DELETE set null)
started_attimestamptzYes—
finished_attimestamptzYes—
manual_statusenum(reading_status)Yes—
manual_started_attimestamptzYes—
manual_finished_attimestamptzYes—
manual_paused_attimestamptzYes—
manual_set_attimestamptzYes—
external_statusenum(reading_status)Yes—
external_status_synced_attimestamptzYes—
updated_attimestamptzNonow()

Indexes: reading_aggregate_book_id_idx, reading_aggregate_user_id_idx

service_credentials ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
servicetextNo—
user_idtextNo—FK → users.id (ON DELETE cascade)
usernametextNo—
password_hashtextNo—
created_attimestamptzNonow()
updated_attimestamptzNonow()

Indexes: service_credentials_user_id_idx

kosync_credentials ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
user_idtextNo—FK → users.id (ON DELETE cascade)
usernametextNo—
secret_hashtextNo—
created_attimestamptzNonow()
updated_attimestamptzNonow()

Indexes: kosync_credentials_username_uniq (unique), kosync_credentials_user_id_uniq (unique)

app_settings ​

ColumnTypeNullableDefaultNotes
keytextNo—PK
valuejsonbNo—
updated_attimestamptzNonow()

upload_registry ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
checksumtextNo—
user_idtextNo—FK → users.id (ON DELETE cascade)
filenametextNo—
created_attimestamptzNonow()

hardcover_sync_log ​

ColumnTypeNullableDefaultNotes
iduuidNorandom()PK
book_iduuidNo—FK → books.id (ON DELETE cascade)
user_idtextNo—FK → users.id (ON DELETE cascade)
hardcover_user_book_idintegerYes—
hardcover_read_idintegerYes—
last_statustextYes—
last_progressnumeric(5,4)Yes—
last_ratingnumeric(3,1)Yes—
last_synced_attimestamptzNo—
created_attimestamptzNonow()
updated_attimestamptzNonow()

Indexes: hardcover_sync_log_status_idx, hardcover_sync_log_last_synced_at_idx, hardcover_sync_log_user_id_idx

Notes ​

  • books.language holds a canonical lowercase ISO 639-1 code (e.g. en, fr); arbitrary input is normalized by services/api-hono/src/lib/languages.ts.
  • book_files.format, book_metadata_candidates.source, service_credentials.service, and hardcover_sync_log.last_status are free-text columns (not Postgres enums) even though they hold format/source/status-like values.
  • books.search_vector and books.possible_duplicate_of are internal: the bookColumns projection omits both, so no API response carries the FTS column or a raw duplicate FK (the inbox detail exposes only a visibility-checked possibleDuplicate object).
  • reading_progress, reading_progress_history, and reading_aggregate use ON DELETE SET NULL on book_id so reading history survives a book deletion, while their user_id cascades (or is set null for history).
  • Better Auth owns users, sessions, accounts, verifications and api_keys — they are declared in services/api-hono/src/db/auth-schema.ts and are not listed above.
  • The trigram indexes (*_trgm_idx) require the pg_trgm extension; the full-text *_search_vector_idx is a GIN index over the generated tsvector.

Relationships ​

mermaid
erDiagram
    users ||--o{ books : "has many"
    books ||--o{ book_files : "has many"
    books ||--o{ book_metadata_candidates : "has many"
    books ||--o{ reading_progress : "has many"
    users ||--o{ reading_progress : "has many"
    books ||--o{ reading_progress_history : "has many"
    users ||--o{ reading_progress_history : "has many"
    users ||--o{ reading_aggregate : "has many"
    books ||--o{ reading_aggregate : "has many"
    users ||--o{ service_credentials : "has many"
    users ||--o{ kosync_credentials : "has many"
    users ||--o{ upload_registry : "has many"
    books ||--o{ hardcover_sync_log : "has many"
    users ||--o{ hardcover_sync_log : "has many"