Skip to content
jhao0413Public

About

A modern, browser-based EPUB reader application with local storage support.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Latest commit

ย 

History

109 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Leaf Nest

Notice: This app reads locally imported EPUB files only โ€” no URLs, web links, online catalogs, or remote sources of any kind are supported or fetched. The author does not condone any use that violates the rights of authors, publishers, or other rights holders. Built for personal learning; not for commercial use or profit.

Leaf Nest is a browser-based EPUB reader and note-taking app, designed for private reading workflows with local-first storage.

English | ็ฎ€ไฝ“ไธญๆ–‡

Screenshots

Screenshot

Features

  • ๐Ÿ” Authentication: Email/password sign-up and sign-in with cookie-based sessions (better-auth).
  • ๐Ÿ“š EPUB import + library management: Import .epub files in the browser, show covers + metadata, and support batch delete with manage mode.
  • ๐Ÿ“– Two reading layouts: Automatic mobile single-column mode and desktop double-column mode, switchable at runtime.
  • ๐Ÿ“‘ Chapter navigation + progress persistence: Restore chapter/page and text anchor from the synced reading state.
  • ๐ŸŽจ Reading customization: Font size, font family, line-height/formatting, theme (light/dark), and layout mode controls.
  • ๐Ÿ” In-book full-text search: Build per-book text index, cache it locally, and jump to match positions.
  • ๐Ÿ–Š๏ธ Notes & highlights: Highlight or underline selected text and attach notes; edit note content and color; delete entries.
  • ๐Ÿ“š Reading Notes view: Central notes list with per-book grouping and quick jump back to reader.
  • ๐ŸŒ Bilingual UI: English / ็ฎ€ไฝ“ไธญๆ–‡ with persistence.
  • ๐ŸŽฎ Reader ergonomics: Keyboard shortcuts for navigation (arrows/space) and search shortcut (Ctrl/Cmd + K).
  • โ˜๏ธ Service-backed architecture: Hono API + PostgreSQL + S3-compatible object storage with web-first reading flows.
  • ๐Ÿ“ฑ Responsive interface: Sidebar reader pages and toolbar adapt to desktop/mobile usage.

Tech Stack

  • Framework: Vite+ + React Router v7 + React 19
  • Backend: Hono + better-auth + Drizzle ORM
  • Database: PostgreSQL
  • Object Storage: S3-compatible storage / RustFS
  • Client Shell: Tauri v2 for Android and desktop packaging
  • State Management: Zustand v5
  • Styling: Tailwind CSS 4.x + HeroUI + Radix UI
  • Validation: Zod
  • Code Quality: Oxlint + Oxfmt via Vite+
  • Internationalization: Client-side i18n provider
  • Theme: Custom client-side theme provider
  • Package Manager: pnpm 10.34.3
  • Core Libraries: JSZip, lucide-react, framer-motion/motion, dayjs

Getting Started

Prerequisites

  • Node.js 22.12+
  • pnpm 10.34.3
  • Docker Desktop, or another Docker Engine with Compose support

Installation

# Clone the repository
git clone git@github.com:jhao0413/leaf-nest.git
cd leaf-nest

# Install dependencies
pnpm install

Development

Run the one-time development setup. It creates .env with a random authentication secret, starts PostgreSQL and RustFS, creates the object-storage bucket, and applies database migrations:

pnpm dev:setup

Then start the frontend and API:

pnpm dev

After the first-time setup, daily development needs only one command. It ensures PostgreSQL, RustFS, and the bucket are running, then starts both application processes with hot reload:

pnpm start

This starts:

You can verify the backend bootstrap with http://localhost:8787/api/health

On later sessions, Docker Desktop normally restarts the infrastructure automatically. pnpm start ensures the infrastructure is running without recreating .env or applying database migrations; use pnpm stop to stop PostgreSQL and RustFS without deleting their data.

Development commands:

  • pnpm dev:setup: idempotent first-time setup; preserves an existing .env secret and existing data.
  • pnpm start: daily development entry point; starts the infrastructure, frontend, and backend with hot reload.
  • pnpm dev:infra: starts PostgreSQL, RustFS, and bucket initialization.
  • pnpm dev:infra:stop: stops PostgreSQL and RustFS without deleting their volumes.
  • pnpm db:migrate: manually applies pending database migrations.
  • pnpm dev: starts the Vite frontend and Hono API in watch mode.

Local development ports:

  • Frontend: 5173
  • API: 8787
  • PostgreSQL: 5432
  • RustFS S3 API: 9000
  • RustFS Console: 9001

If setup reports that Docker is unavailable, start Docker Desktop and rerun pnpm dev:setup. If a port is already in use, stop the conflicting local service before retrying.

Self-hosted Deployment

A complete self-hosted deployment needs a host with Docker Compose. docker-compose.yml includes the app, database migration, PostgreSQL, RustFS, and bucket initialization services by default. Start the complete local stack without creating an .env file:

docker compose up -d --pull always

The default URL is http://localhost:8787. For direct access through a server's LAN or public IP, create .env and set the host once:

SELF_HOST_PUBLIC_HOST=192.168.x.x
LEAF_NEST_TAG=v0.4.0

Compose derives the app URL, Better Auth URL, and browser-facing RustFS URL from that host. For HTTPS, reverse proxies, custom ports, or separate domains, override them with SELF_HOST_APP_URL, SELF_HOST_BETTER_AUTH_URL, and SELF_HOST_S3_PUBLIC_ENDPOINT.

Bundled PostgreSQL and RustFS start by default. Only disable a bundled dependency when replacing it with an external service:

  • External PostgreSQL: set LOCAL_DB_REPLICAS=0 and SELF_HOST_DATABASE_URL
  • External S3: set LOCAL_STORAGE_REPLICAS=0, SELF_HOST_S3_ENDPOINT, and SELF_HOST_S3_PUBLIC_ENDPOINT
  • Both external: set both replica counts to 0

When using external object storage, create S3_BUCKET before starting the app; storage-init only initializes bundled RustFS. Production deployments should also override the default POSTGRES_PASSWORD, RUSTFS_ACCESS_KEY, and RUSTFS_SECRET_KEY. Compose derives the local database URL from the same POSTGRES_* values; use URL-safe characters in the password.

BETTER_AUTH_SECRET is optional for a single-instance Docker deployment because it is generated and persisted on first start. The complete stack requires Docker Compose 2.20.0 or later.

Common service-management commands:

  • pnpm deploy:start: equivalent to docker compose up -d --pull always.
  • pnpm deploy:status: shows container status.
  • pnpm deploy:logs: follows application logs.
  • pnpm deploy:stop: stops and removes containers while preserving data volumes.

The generated authentication secret is stored in the persistent app-data volume, so container recreation reuses it. Set BETTER_AUTH_SECRET explicitly for multi-instance deployments or externally managed secrets. Removing app-data invalidates existing sessions because a new secret will be generated.

Default published ports when the bundled services are enabled:

  • App: 8787
  • RustFS S3 API: 9000
  • RustFS Console: 9001

PostgreSQL is kept on the internal Docker network. If you publish your own app image, push a Git tag such as v0.4.0; the GitHub Actions workflow publishes jhao0413/leaf-nest:<tag> and jhao0413/leaf-nest:latest to Docker Hub using DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets.

Database Workflow

pnpm db:generate
pnpm db:migrate
  • pnpm db:generate: generate SQL migrations from the Drizzle schema in server/src/db/schema
  • pnpm db:migrate: apply generated migrations to the configured PostgreSQL database

Local Production Build

pnpm build
pnpm preview

Tauri Android Client

The Android client uses Tauri v2 to package the existing React reader into a native Android WebView shell while continuing to use a deployed Leaf Nest API.

Prerequisites for Android builds:

  • Rust stable MSVC toolchain on Windows
  • Android Studio with Android SDK Platform, Platform-Tools, Build-Tools, Command-line Tools, and NDK
  • JAVA_HOME, ANDROID_HOME, and NDK_HOME configured for the current shell
# Install Tauri dependencies
pnpm install

# Generate the Android project the first time
pnpm tauri:android:init

# Run on an attached Android device or emulator
$env:VITE_API_BASE_URL='https://reader.example.com'; pnpm tauri:android:dev

# Build an Android package
$env:VITE_API_BASE_URL='https://reader.example.com'; pnpm tauri:android:build

For the backend that serves the Android client, set TRUSTED_CLIENT_ORIGINS=tauri://localhost and use an HTTPS BETTER_AUTH_URL for cross-origin credentials.

Environment Variables

Source development expects these backend variables in .env:

  • APP_URL
  • API_PORT
  • DATABASE_URL
  • BETTER_AUTH_SECRET
  • BETTER_AUTH_URL
  • TRUSTED_CLIENT_ORIGINS
  • S3_ENDPOINT
  • S3_PUBLIC_ENDPOINT
  • S3_REGION
  • S3_BUCKET
  • S3_ACCESS_KEY_ID
  • S3_SECRET_ACCESS_KEY
  • S3_FORCE_PATH_STYLE

The frontend also reads VITE_API_BASE_URL. Leave it empty for same-origin browser usage; set it to the deployed app/API origin when building the Tauri Android client.

For Docker Compose deployments, the app container maps optional SELF_HOST_* variables to its runtime APP_URL, DATABASE_URL, BETTER_AUTH_URL, and S3 endpoints. This keeps local development defaults intact while allowing remote self-host deployments to use a public app domain and a browser-reachable object-storage endpoint. TRUSTED_CLIENT_ORIGINS is passed through for packaged clients. BETTER_AUTH_SECRET may be supplied explicitly; otherwise the container persists an automatically generated value in app-data.

Code Quality

pnpm lint
pnpm fmt
pnpm check
pnpm test

Project Structure

leaf-nest/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ main.tsx             # Vite+ entrypoint
โ”‚   โ”œโ”€โ”€ App.tsx              # BrowserRouter + route shell
โ”‚   โ”œโ”€โ”€ navigation.ts        # react-router-dom wrappers
โ”‚   โ”œโ”€โ”€ app/                 # Route components consumed by React Router
โ”‚   โ”‚   โ”œโ”€โ”€ page.tsx         # Home (book library)
โ”‚   โ”‚   โ”œโ”€โ”€ reader/[id]/     # Reader page
โ”‚   โ”‚   โ”œโ”€โ”€ notes/           # Notes list and per-book notes pages
โ”‚   โ”‚   โ””โ”€โ”€ settings/        # Language and global settings
โ”‚   โ”œโ”€โ”€ components/          # React components
โ”‚   โ”‚   โ”œโ”€โ”€ AuthCard.tsx     # Sign-in / sign-up form
โ”‚   โ”‚   โ”œโ”€โ”€ AuthGate.tsx     # Auth guard wrapper
โ”‚   โ”‚   โ”œโ”€โ”€ ClientLayout.tsx # Sidebar + main layout
โ”‚   โ”‚   โ”œโ”€โ”€ Sidebar.tsx      # Navigation sidebar
โ”‚   โ”‚   โ”œโ”€โ”€ Renderer/        # EPUB renderers (single/double column)
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ SearchModal.tsx       # Full-book search modal
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Toolbar/              # Reader toolbar controls
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ HighlightPopup.tsx    # Highlight create/edit overlays
โ”‚   โ”‚   โ””โ”€โ”€ ui/              # Shared UI primitives
โ”‚   โ”œโ”€โ”€ hooks/               # Custom React hooks
โ”‚   โ”œโ”€โ”€ i18n/                # Client-side i18n provider + locale config
โ”‚   โ”œโ”€โ”€ lib/                 # Client-side libraries
โ”‚   โ”‚   โ”œโ”€โ”€ api/             # API client + typed contracts
โ”‚   โ”‚   โ”œโ”€โ”€ auth/            # better-auth client + session store
โ”‚   โ”‚   โ”œโ”€โ”€ binary/          # Book binary source abstraction (remote S3)
โ”‚   โ”‚   โ””โ”€โ”€ repositories/    # Data repositories (books, highlights, reading)
โ”‚   โ”œโ”€โ”€ plugins/             # HeroUI plugin config
โ”‚   โ”œโ”€โ”€ store/               # Zustand stores
โ”‚   โ”œโ”€โ”€ theme/               # Light/dark theme provider
โ”‚   โ”œโ”€โ”€ utils/               # Utility functions
โ”‚   โ”‚   โ”œโ”€โ”€ chapterLoader.ts           # Chapter content loading
โ”‚   โ”‚   โ”œโ”€โ”€ chapterParser.ts           # Chapter parsing
โ”‚   โ”‚   โ”œโ”€โ”€ epubStructureParser.ts     # EPUB metadata + TOC parsing
โ”‚   โ”‚   โ”œโ”€โ”€ fullBookTextIndexer.ts     # Full-book full-text indexer
โ”‚   โ”‚   โ”œโ”€โ”€ readingProgressManager.ts  # Debounced progress persistence
โ”‚   โ”‚   โ””โ”€โ”€ highlightRenderer.ts       # Render highlights into reader iframe
โ”‚   โ””โ”€โ”€ messages/            # Translation files (en, zh)
โ”œโ”€โ”€ server/
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ index.ts         # Entry: loads env, wires services, starts Hono
โ”‚       โ”œโ”€โ”€ app.ts           # Hono app factory: routes, auth, static serving
โ”‚       โ”œโ”€โ”€ env.ts           # Zod-validated env schema
โ”‚       โ”œโ”€โ”€ db/schema/       # Drizzle schema (auth, books, files, progress, highlights)
โ”‚       โ”œโ”€โ”€ lib/             # Services (auth, books, reading, storage, db)
โ”‚       โ””โ”€โ”€ routes/          # REST API routes (books, reading, health)
โ”œโ”€โ”€ public/                  # Static assets
โ”œโ”€โ”€ src-tauri/               # Tauri v2 shell for Android and desktop packages
โ”œโ”€โ”€ docker-compose.yml       # Docker deployment stack (app, postgres, RustFS)
โ”œโ”€โ”€ Dockerfile               # Multi-stage Node.js build
โ””โ”€โ”€ vite.config.ts           # Vite+ / Vite shared configuration

Deployment Notes

  • The bundled Node server already falls back to dist/index.html for SPA routes such as /notes/123 and /reader/abc.
  • Signed object URLs are generated from S3_PUBLIC_ENDPOINT, so that endpoint must be reachable from browsers.
  • Remote object assets such as covers are expected to load cross-origin, so the frontend no longer enables cross-origin isolation headers by default.

Implementation Notes

Database & Storage

The app stores primary data on the backend:

  • PostgreSQL: users, books, reading progress, highlights, and related metadata
  • S3-compatible object storage: EPUB binaries and remote cover objects
  • Browser state: reader UI state and in-memory search helpers for the active session

Authentication

  • Email/password sign-up and sign-in powered by better-auth with a Drizzle adapter.
  • Sessions are cookie-based and managed server-side.
  • All API routes (except /api/health and /api/auth/*) require a valid session.
  • The frontend uses AuthGate to show the sign-in card for unauthenticated users.

EPUB Rendering

  • SingleColumnRenderer and DoubleColumnRenderer are both iframe-based and support responsive behavior.
  • Double-column mode pre-renders page width/position data for horizontal page navigation.
  • A lightweight chapter-to-chapter render pipeline handles parsing, font/theme application, and image-ready pagination.
  • Menu + TOC panel supports quick chapter jump.

Reading Progress

The ReadingProgressManager automatically saves:

  • Current chapter index
  • Current page number
  • Text anchor for precise position restoration
  • Reading percentage
  • Last read timestamp

Notes

  • Highlights can be created with color and style variants and can be edited/deleted.
  • Notes are synchronized through the backend APIs and shown in a dedicated notes center.
  • Deleting a book also deletes its related highlights and index data.

Full-text Search

  • Reader can trigger full-book indexing (background) and query across all indexed chapters.
  • Search results show surrounding context and jump directly to the matched chapter/page.

Shortcuts

  • Arrow keys and space bar for navigation in reader.
  • Ctrl/Cmd + K opens search on supporting platforms.

About

A modern, browser-based EPUB reader application with local storage support.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages