The repository contains two runtime applications:
backend/A local Express proxy that fronts Suwayomi and exposes a simpler REST API for the UI.manga-nexus/The Electron desktop app. It contains:- the Electron main process
- the preload bridge
- the React/Vite renderer
At runtime, the stack looks like this:
React renderer -> Express proxy (3001) -> Suwayomi (4567) -> source extensions
Electron sits beside that flow and is responsible for booting and supervising both local services.
- Entry:
manga-nexus/src/main.jsx - Main app shell and state:
manga-nexus/src/App.jsx - Reader session logic:
manga-nexus/src/components/reader/Reader.jsx - Extension management view:
manga-nexus/src/components/extensions/ExtensionsTab.jsx - Supporting presentational components:
manga-nexus/src/components/**,manga-nexus/src/views/**
The renderer is not thin. It owns:
- UI state
- persisted user state
- chapter download queue
- offline chapter IndexedDB storage
- app navigation
- reader position tracking
- Entry:
backend/server.js
The backend is a translator and stabilizer layer. It adds:
- REST endpoints tailored to the UI
- retries around Suwayomi requests
- in-memory caching for extensions, search, manga, and page lists
- image proxying
- extension install/update/uninstall helpers
manga-nexus/electron-main.js is effectively the runtime supervisor.
Responsibilities:
- single-instance lock
- window/tray lifecycle
- settings persistence for Electron-only preferences
- Java/JRE discovery or installation
- Suwayomi JAR discovery or download
- optional Windows service install/uninstall
- backend process startup via
utilityProcess.fork - Suwayomi startup/health wait loop
- updater integration with
electron-updater - IPC handlers exposed through
preload.js
The preload contract in manga-nexus/preload.js gives the renderer access to:
- window controls
- service restart/ensure operations
- service installation status
- current desktop platform so Windows-only UI can stay hidden elsewhere
- data directory/runtime info
- source verification popups for sites that require a browser challenge
- packaged app updater actions
Packaged builds use electron-updater from the Electron main process. Updates download in the background while the renderer stays interactive. Once downloaded, the banner tells the user the update will install when the app is closed; an explicit Restart now button is still available for immediate install. Windows packaging uses a one-click NSIS installer so online update installs avoid a manual setup wizard.
There is no external state library. State is centralized in React.
Primary pattern:
DataContextis defined insrc/contexts/DataContext.jsxDataProvideractually lives insrc/App.jsxuseData()is the main access point for shared state/actions
Core persisted state in DataProvider:
libraryhistoryprogressmangaCategoriesreadChaptersreadingTimesettings
Persistence layers:
localStorageStores app/user state such as library, progress, settings, and history.- IndexedDB
Stores offline chapter page payloads in the
chaptersobject store.
Network/service state:
backendOnlinesuwayomiReadysourcesextensionsupdatesdownloadQueue
Primary files:
manga-nexus/src/App.jsxmanga-nexus/src/components/reader/Reader.jsx
Flow:
- User opens manga details in
openManga(). - User opens a chapter in
openChapter(). openChapter()first checks IndexedDB vialoadChapterBlobs().- If offline pages exist, they are used immediately.
- Otherwise the renderer requests
/api/source/:sourceId/chapter/:chapterId. Reader.jsxreceives:- initial pages
- current chapter
- navigation callbacks
- persisted initial page
Reader.jsxderives a flattenedallPageslist from loaded chapters.- In scroll/webtoon mode, an
IntersectionObservertracks the most visible page and persists progress. - In paged mode, keyboard/tap/wheel handlers drive page changes directly.
- When the reader nears the end, it may call
fetchNextChapter()and append the next chapter into the same reading session. - Reader-side next-chapter prefetch now uses an
AbortControllerso stale prefetches can be canceled during teardown/navigation.
Route:
/api/source/:sourceId/manga/:mangaId
Backend behavior:
- tries
fetchManga - falls back to direct
manga(id)query - tries chapter list query
- falls back to
fetchChapters - normalizes chapter metadata for the renderer
Route:
/api/source/:sourceId/chapter/:chapterId
Backend behavior:
- GraphQL
fetchChapterPages - normalizes page URLs with
fixUrl() - caches page arrays in memory using source-aware keys
The reader does not always leave and reopen for the next chapter. Instead:
Reader.jsxwatches a sentinel near the end of scroll mode- or hits navigation in paged mode
- then calls
fetchNextChapter()fromApp.jsx fetchNextChapter()checks IndexedDB first, then calls the backend- both
openChapter()and reader-side prefetch use abort signals to avoid stale async writes after navigation - if successful,
Reader.jsxappends the next chapter intoloadedChapters
There are two distinct image paths:
- Suwayomi/source returns a page or cover URL.
- UI often passes it through
proxyImg(). proxyImg()rewrites local/Suwayomi URLs to/api/img?url=....- Backend
/api/imgonly allows loopback/Suwayomi hosts, then fetches the binary and returns it with cache headers. - Browser/Electron webview handles normal HTTP caching.
This is used for:
- covers
- chapter pages while online
Source and extension icons are different: renderer code normalizes Suwayomi-relative icon paths to absolute http://localhost:4567/... URLs and loads them directly. That avoids routing hundreds of small extension-icon requests through the React dev proxy or backend image proxy.
The extension list is rendered by ExtensionsTab.jsx, which owns its search/filter/sort state, defers search input rendering work, incrementally reveals rows, and uses content-visibility for cheaper offscreen cards.
- Download queue fetches chapter page URLs.
- Each page is fetched in the renderer.
saveChapterBlobs()stores the rawBlobpayloads in IndexedDB.loadChapterBlobs()converts stored blobs into temporary object URLs when reopening a chapter.App.jsxrevokes old blob URLs when pages change or the reader flow unmounts.
Important note: offline cache storage is renderer-managed, not backend-managed.
Some source websites require a browser challenge before Suwayomi can fetch metadata or pages. akaReader treats this as a user-driven verification step rather than a silent bypass:
- The backend returns the source/Cloudflare/challenge error to the renderer.
- The renderer shows a
Verify Sourceaction on manga or chapter failures. - Electron opens the source URL in a dedicated verification
BrowserWindow. - The app waits until the user closes that window.
- The renderer retries the manga/chapter request.
The app also avoids applying its local Content Security Policy to external verification pages so challenge scripts can run normally.
There is no router library.
Navigation is manual state-machine navigation inside App.jsx using values like:
tabview
Common views:
- tabbed home
- browse/source results
- manga detail
- reader
Important consequences:
- back navigation is custom (
goBack()) - keyboard/back-button behavior is custom
- view transitions depend on coordinated state updates, not route URLs
react,react-domUI rendering and state.viteFast dev server and frontend build tool.electronDesktop shell.electron-builderPackaging and distributables.electron-updaterIn-app update checks/downloads for packaged builds.lucide-reactIcon set used throughout the UI.concurrently,wait-onLocal dev orchestration for running Vite and Electron together.
expressHTTP API surface.axiosOutbound HTTP/GraphQL requests to Suwayomi and upstream images.corsCross-origin support for local dev and Electron renderer access.compressionResponse compression when available.helmetBasic hardening headers when available.archiverIntended CBZ/ZIP download packaging.
src/App.jsxis the dominant state and UI container and should be treated as a high-risk file.Reader.jsxhas sophisticated async/state behavior and several observer/timer-driven flows.- Hook dependency arrays are evaluated during render. A callback must be declared before another hook dependency array references it, otherwise production bundles can throw
Cannot access '<minified name>' before initialization. backend/server.jsmixes multiple concerns:- cache layer
- GraphQL adapter
- image proxy
- extension management
- archive download route
Those files should be the first places to document and test whenever behavior changes.