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 | ็ฎไฝไธญๆ
- ๐ Authentication: Email/password sign-up and sign-in with cookie-based sessions (better-auth).
- ๐ EPUB import + library management: Import
.epubfiles 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.
- 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
- Node.js 22.12+
- pnpm 10.34.3
- Docker Desktop, or another Docker Engine with Compose support
# Clone the repository
git clone git@github.com:jhao0413/leaf-nest.git
cd leaf-nest
# Install dependencies
pnpm installRun 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:setupThen start the frontend and API:
pnpm devAfter 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 startThis starts:
- Web app on the local URL printed by
vp dev(by default http://localhost:5173) - Hono API on http://localhost:8787
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.envsecret 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.
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 alwaysThe 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.0Compose 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=0andSELF_HOST_DATABASE_URL - External S3: set
LOCAL_STORAGE_REPLICAS=0,SELF_HOST_S3_ENDPOINT, andSELF_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 todocker 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.
pnpm db:generate
pnpm db:migratepnpm db:generate: generate SQL migrations from the Drizzle schema inserver/src/db/schemapnpm db:migrate: apply generated migrations to the configured PostgreSQL database
pnpm build
pnpm previewThe 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, andNDK_HOMEconfigured 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:buildFor the backend that serves the Android client, set TRUSTED_CLIENT_ORIGINS=tauri://localhost and use an HTTPS BETTER_AUTH_URL for cross-origin credentials.
Source development expects these backend variables in .env:
APP_URLAPI_PORTDATABASE_URLBETTER_AUTH_SECRETBETTER_AUTH_URLTRUSTED_CLIENT_ORIGINSS3_ENDPOINTS3_PUBLIC_ENDPOINTS3_REGIONS3_BUCKETS3_ACCESS_KEY_IDS3_SECRET_ACCESS_KEYS3_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.
pnpm lint
pnpm fmt
pnpm check
pnpm testleaf-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
- The bundled Node server already falls back to
dist/index.htmlfor SPA routes such as/notes/123and/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.
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
- 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/healthand/api/auth/*) require a valid session. - The frontend uses
AuthGateto show the sign-in card for unauthenticated users.
- 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.
The ReadingProgressManager automatically saves:
- Current chapter index
- Current page number
- Text anchor for precise position restoration
- Reading percentage
- Last read timestamp
- 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.
- 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.
- Arrow keys and space bar for navigation in reader.
Ctrl/Cmd + Kopens search on supporting platforms.
