miwayomi · Docs

miwayomi.

A self-contained server that runs Aniyomi/Tachiyomi-format catalog extensions on the JVM — with no Android device, no emulator, no cloud. This page documents everything: how it works, its technical requirements, its API, and how to make it yours.

mi·wa·yo·mi — “beautiful reading”

Get miwayomi

Pick your platform — both files come from the latest release and the app auto-updates itself.

🖥️ Windows

Native installer — Start Menu & desktop shortcut.

⬇ Download installer (.exe)

🐧 🍎 Any OS (JAR)

Cross-platform JAR — needs JDK 21.

⬇ Download JAR

Run: java -jar miwayomi-all.jar

All releases: github.com/miwayomi/miwayomi/releases · the Windows installer can also bundle a JRE for a fully standalone install.

The story behind the name

Every project deserves a story, and miwayomi got a good one by accident. It started the way most self-hosted projects start: with a problem and a grudge. The original plan was modest — a self-hosted anime player. It was meant to sit on top of Suwayomi, but that project kept hanging and crashing, eating RAM and fighting at every turn. After too many nights of a frozen server, the call was made: “fine, I’ll build my own.”

So miwayomi was rebuilt from a different angle — a lean execution engine modeled on the ideas of Aniyomi (itself born from Tachiyomi): the same extension format and the same familiar source API, but reimagined as a pure JVM service that needs no Android at all.

The name came from a manga. A character named Miwa caught the eye — it just sounded right — and turned into miwayomi without knowing what it meant. When it was finally looked up, it turned out to mean roughly “beautiful reading” (mi + yomi). A happy accident, and the best kind: the name picked the project before the project picked the name.

What miwayomi is (and is not)

miwayomi ismiwayomi is not
An execution engine for Tachiyomi/Aniyomi-format extensionsA content host or “piracy box”
A JVM server (Ktor) that runs extensions without AndroidAn emulator or a phone-in-the-cloud
A REST API + web UI over your installed sourcesA fork, rebrand, or replacement of any client
A teaching tool: learn to write your own compatible sourcesA distributor of extensions or copyrighted media
Core rule: extensions are third-party software. miwayomi runs them and exposes their catalogs. Whatever a source does, it does on its own behalf. You are responsible for the extensions you install and the content you access.

Design philosophy

Technical requirements

RequirementMinimumNotes
JDK21 (Temurin recommended)Gradle downloads itself via the wrapper.
RAM~512 MB heapRuns with -Xmx512m; measured ~160–170 MB RSS at rest. Adjust with MIWAYOMI_MEM.
DiskA few hundred MBExtensions (.apk + converted .jar), image cache, SQLite DB.
FlareSolverrOptionalAuto-solves Cloudflare challenges (miwayomi ships no browser).
OSLinux / macOS / WindowsJVM-based; tested on Linux.
Port4567 (default)Configurable with --port.

Build toolchain

How it works

Tachiyomi/Aniyomi extensions are APK files that announce their catalog classes in the manifest under meta-data (tachiyomi.extension.class / tachiyomi.animeextension.class). miwayomi turns those APKs into runnable sources on the JVM:

Extension (APK or repo-published JVM jar)
  → 1. apk-parser reads AndroidManifest.xml
  → 2. use the repo's JVM jar when published; otherwise dex2jar converts classes.dex to a .jar
  → 3. JarFixer auto-repairs bytecode on first load (invalid <init> owners from DEX→JVM)
  → 4. ChildFirstURLClassLoader loads the classes
  → 5. SourceFactory / Source instantiated & registered
  → 6. REST API + WebUI expose every catalog

Because the mechanism is generic, it works for any extension in that format — the format is the contract, not the vendor.

Architecture

ModuleRole
android-compat/Minimal Android shim: the android.*/androidx.* classes extensions reference, plus the build-time android.jar stub.
core-common/The network heart: OkHttp wiring, interceptors, cookie jar, SQLite store, FlareSolverr client, JS engine.
source-api/The Aniyomi source API compiled for the JVM: source interfaces and models extensions implement.
server/The Ktor application: extension pipeline, source managers, REST API, streaming proxy, WebUI.
data/Runtime data: data/extensions/*.apk (+ converted or repo-published .jar), preferences, SQLite + image cache.

Codebase map (file by file)

server/ — the application

server/src/main/kotlin/miwayomi/ ├── Main.kt Entry point: args, DI, Ktor. ├── Config.kt ServerConfig + CLI (--port, --data, --flaresolverr, ...). ├── di/ AppModule (Injekt) + ConfigHolder. ├── source/SourceManagers.kt Manga/Anime source registries. ├── extension/ │ ├── ExtensionManager.kt Loads/unloads APKs (or repo JVM jars); auto-fixes jars on load. │ ├── PackageTools.kt apk-parser, dex2jar, class-name resolution. │ ├── JarFixer.kt ASM bytecode repair (invalid <init> owners from DEX→JVM); auto-applied on first load. │ └── ChildFirstURLClassLoader.kt ├── builtin/ DemoSource (offline) + MockCfSource. └── api/ ├── ApiRoutes.kt Plugins, static, /health, /sources, sub-routers. ├── MangaRoutes.kt Manga catalog endpoints. ├── AnimeRoutes.kt Anime catalog/episodes/videos endpoints. ├── StreamingRoutes.kt /proxy /hls /dash /dashseg + image cache. ├── StreamProxy.kt HLS/DASH manifest rewriting. ├── ProxyHelpers.kt Header parsing, per-source clients, URL normalization. ├── MimeTypes.kt MIME inference when the CDN lies. ├── ImageCache.kt Disk cache for proxied images. ├── ExtensionRoutes.kt Repo index (JSON + protobuf), install (prefers JVM jars)/uninstall. ├── SourcePrefsRoutes.kt Source preferences GET/POST. ├── FavoritesRoutes.kt Favorites + last-read progress. ├── ApiHelpers.kt Shared helpers + chunked proxy stream. ├── Dtos.kt / Mappers.kt @Serializable models + converters. └── KeiIndexProto.kt Protobuf (index.pb, gzip) repo parser.

core-common/ — the network heart

core-common/.../network/ ├── NetworkHelper.kt Builds the shared OkHttpClient (no gzip/brotli by default); owns the SQLite store. ├── JvmCookieJar.kt Persistent cookie jar backed by SQLite. ├── SqliteStore.kt SQLite: kv_store, cookies, favorites. ├── CookieCodec.kt Cookie ↔ row (de)serialization. ├── CfResolvedUa.kt Persists host → browser UA for resolved hosts. ├── CloudflareChallengeException.kt ├── FlareSolverr.kt Auto-solver client (FlareSolverr /v1). ├── JavaScriptEngine.kt GraalJS (QuickJs/Duktape stand-ins). └── interceptor/ Cloudflare, JSON-fix, UA, rate-limit, uncaught.

source-api/ — the extension contract

source-api/.../ ├── source/ MangaSource, CatalogueSource, SourceFactory, HttpSource, models. ├── animesource/ AnimeSource, AnimeHttpSource, models (SAnime, SEpisode, Video…). └── util/ JsoupExtensions, JsonExtensions, RxExtension.

android-compat/ — the Android shim

android-compat/src/main/kotlin/ ├── android/app/Application.kt Application : ContextWrapper : Context. ├── android/content/ · os/ · webkit/ · view/ · widget/ · graphics/ · util/ ├── androidx/preference/Preference.kt Real preference tree. ├── app/cash/quickjs/QuickJs.kt GraalJS-backed. └── com/squareup/duktape/Duktape.kt GraalJS-backed.

The Web UI

server/src/main/resources/webui/ ├── index.html Single page shell: header, sidebar, modals. ├── style.css Crunchyroll-style dark theme. ├── app.js The whole SPA: views, API calls, i18n, players. ├── lang/en.json · es.json Translations (add a file to add a language). └── docs/ This documentation + assets.

REST API

Base URL: http://<host>:4567/api/v1 — JSON in, JSON out.

MethodPathPurpose
GET/healthLiveness + source counts.
GET/sourcesAll installed manga & anime sources.
GET/updateCurrent version, latest GitHub release, update status.
GET/manga/{id}/popular|latest|search?query=&page=Manga catalog.
GET/manga/{id}/details|chapters|pages?url=Manga data.
GET/anime/{id}/popular|latest|searchAnime catalog.
GET/anime/{id}/details|episodes|seasons|hosters|videos|hosterVideosAnime data + streams.
GET/proxy|/hls|/dash|/dashsegMedia proxy (images are cached on disk).
GET/POST/sources/{id}/prefsSource preferences.
GET/POST/DELETE/favorites…Favorites + progress.
GET/POST/DELETE/watchAnime watch history (continue watching / resume).
GET/POST/extensions/repo|installed|repos|install|uninstallExtension manager (installed list + repo URLs persisted in the database).
# Health
curl http://localhost:4567/api/v1/health
# → {"status":"ok","service":"miwayomi","mangaSources":7,"animeSources":13}

# Popular manga from a source, page 1
curl "http://localhost:4567/api/v1/manga/8448310129093543312/popular?page=1"

# Chapters of a manga (url = the source's own URL, percent-encoded)
curl "http://localhost:4567/api/v1/manga/8448310129093543312/chapters?url=%2Fmanga%2F1"
Source IDs are 64-bit values and are returned as strings. Keep them as strings in clients — JS numbers lose precision above 2^53.

The Web UI

The web UI is a single-page app with a Crunchyroll-style structure: top navigation, a filterable source sidebar, a home with hero and rows, a detail page with hero and numbered episode/chapter cards, a continuous manga reader, and a video player with an episode list.

miwayomi home
miwayomi screenshot
Source catalog
miwayomi screenshot
Catalog row
miwayomi screenshot
Series detail
miwayomi screenshot
Detail with chapters
miwayomi screenshot
Chapters
miwayomi screenshot
Reader / player
These screenshots are for illustration purposes only. All cover art, titles, extension names, and related content belong to their respective owners.

Highlights

🔍 Filterable sidebar

Filter sources by name and toggle All / Manga / Anime.

🏠 Home with hero

Hero banner plus horizontal rows of source cards.

🖼️ Chapter thumbnails

Each chapter shows its first page, lazy-loaded and cached.

▶ Player with episodes

Video player with a side episode list and quality selector.

📖 Continuous reader

Manga pages flow with no gaps — ideal for manhwa.

🌐 i18n

English & Spanish; add a lang/<code>.json for more.

▶ Continue watching

Home row from SQLite history — resumes each episode where you left off.

🔀 Player extras

Invert episode order, auto-play next, auto-select source, restart.

📡 AniList sync

Connect your account to push watched-episode progress.

Customizing the UI

Everything about the interface lives in server/src/main/resources/webui/. You never touch the backend to change the look — edit these files and rebuild:

FileWhat it controls
index.htmlThe page shell: header, navigation, sidebar, modals, script tags.
style.cssEverything visual. All colors are CSS variables at the top (:root).
app.jsBehavior: views, rendering, API calls, i18n, players.
lang/*.jsonUser-visible strings, one file per language.

Change the theme (colors)

Open style.css and edit the variables at the top. For example, to go from the orange accent to a violet one:

/* style.css :root */
:root {
  --accent: #7c3aed;      /* was #f47521 */
  --accent-hi: #a78bfa;   /* was #ff8c3b */
}

The background, panels, borders, text, and buttons all reference variables, so one change propagates everywhere.

Add a language

# 1. Copy the English file
cp lang/en.json lang/fr.json
# 2. Translate the values (keep the keys), then register it:
#    in index.html: <option value="fr">Français</option>
#    in app.js:     AVAILABLE_LANGS = ["en","es","fr"]

Add a new view or section

  1. Write a render function in app.js that fills #content (look at renderHome or renderDetail as templates).
  2. Add a button/nav item in index.html that calls it.
  3. Style the new elements in style.css reusing the variables.
  4. Any user-visible text goes through t("key") and is added to lang/en.json and lang/es.json.

Apply your changes

cd /home/asking/Escritorio/miwayomi
./gradlew :server:installDist   # repackages the WebUI into the distribution
./start.sh                       # restart
After changing app.js, always validate first: node --check server/src/main/resources/webui/app.js. If the port is busy after a rebuild, kill the old process: pkill -9 -f "server/build/install/server/lib".

Building and running

Requirements

Quick start (desktop / JAR)

java -jar miwayomi-all.jar (from the Releases page) just works: it picks a free port, starts and opens your default browser. The launcher ./miwayomi / miwayomi.bat opens a dedicated app window (Chrome/Edge --app) and stops the server when you close it.

java -jar miwayomi-all.jar      # any OS — auto port + opens the browser
./miwayomi                      # desktop launcher (app window)

Force a port with --port 4567; --no-open disables opening a browser.

Auto-update

On startup the server checks GitHub for a newer release: if one exists it downloads the new JAR and the WebUI shows a banner ("New version available — close and relaunch to apply"). The next launch applies it. Status: GET /api/v1/update.

Windows installer

packaging/build-installer.sh (needs NSIS: sudo apt install nsis / brew install nsis / yay -S nsis) produces packaging/dist/miwayomi-setup-<version>.exe. Pass JRE_ZIP=temurin-21-windows-x64.zip to bundle a JRE for a standalone installer. A GitHub Actions workflow also builds it automatically on each release.

Lightweight build (recommended for a VPS)

./gradlew :server:installDist
./gradlew --stop                # frees the Gradle daemons (RAM)
./start.sh                      # uses the distribution if present (headless)

The JVM runs with -Xmx512m -Xms64m -XX:MaxMetaspaceSize=256m -XX:+UseSerialGC. Override RAM with MIWAYOMI_MEM="-Xmx768m" ./start.sh.

Docker (recommended for servers / VPS)

docker compose up -d      # builds miwayomi + FlareSolverr (from source, in a venv)

WebUI/API on http://localhost:4567, FlareSolverr on http://localhost:8191; persistent data (SQLite, extensions, cookies) lives in the named volume miwayomi-data. The Dockerfile builds the fat JAR with a JDK stage and runs it with a slim JRE (no JDK, no browser). FlareSolverr is built from source inside a Python virtualenv (docker/flaresolverr.Dockerfile) and bundles its own headless Chromium engine — required by FlareSolverr, not part of miwayomi. Tune JVM memory with the JAVA_OPTS env var; change the solver URL with FLARESOLVERR_URL (empty disables it).

Prebuilt images are published to GHCR on every push to main and on every release — multi-arch (linux/amd64, linux/arm64), no local build needed: ghcr.io/miwayomi/miwayomi and ghcr.io/miwayomi/flaresolverr (latest, plus version tags like 0.3.0). Run the app image alone: docker run -d -p 4567:4567 -v miwayomi-data:/data -e FLARESOLVERR_URL=http://127.0.0.1:8191 ghcr.io/miwayomi/miwayomi:latest.

Dev mode

./gradlew :server:run --args="--data ./data --port 4567 --flaresolverr http://127.0.0.1:8191"

CLI options

FlagDefaultMeaning
--port, -pauto (free)Listen port; omit for an automatic free port.
--host, -h0.0.0.0Listen address.
--data, -d./dataData directory (extensions, prefs, cache).
--flaresolverr, -fhttp://127.0.0.1:8191FlareSolverr URL (blank disables).
--no-openoffDo not open a browser on start (headless).

Verify

curl http://localhost:4567/api/v1/health

Then open http://localhost:4567.

Creating your own extension

Legally safe, by design. miwayomi does not tell you to download or redistribute anyone’s extensions. This section teaches you how to write your own compatible source and load it. Respect the terms of service of the sites you write sources for, and never use this to infringe copyright.

Option A — a built-in source (fastest)

Every source is an object implementing the MangaSource contract. Write a Kotlin class in server/src/main/kotlin/miwayomi/builtin/ — exactly like DemoSource.kt — rebuild, and it appears in /sources.

Option B — a real extension APK

Three things: a manifest, a source class, and a build that produces an APK.

1. The manifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <meta-data
            android:name="tachiyomi.extension.class"
            android:value="com.example.demolib.DemoLibFactory" />
    </application>
</manifest>

The class must be a SourceFactory (createSources(): List<MangaSource>) or a direct MangaSource subclass.

2. The source (original example)

package com.example.demolib

import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.model.*

class DemoLib : HttpSource() {
    override val name = "DemoLib"
    override val lang = "en"
    override val baseUrl = "https://demo-manga.example"
    override val supportsLatest = true

    override fun getFilterList() = FilterList()

    override fun popularMangaRequest(page: Int) = GET("$baseUrl/popular?page=$page", headers)
    override fun popularMangaParse(response: Response) =
        response.use { parseMangaPage(it) }
    // ... search, latest, details, chapters, pages ...
}

class DemoLibFactory : SourceFactory {
    override fun createSources() = listOf(DemoLib())
}

3. Build & install

Streaming internals

All proxies stream in 64 KB chunks (no full-file buffering). MimeTypes.kt infers the correct content type even when the CDN sends application/octet-stream. Images are cached to disk (data/cache/images/) so thumbnails, covers, and manga pages load instantly on repeat visits; video is deliberately not cached so Range/206 stays correct.

Cloudflare challenges

Some sources sit behind Cloudflare’s anti-bot. miwayomi unlocks them via FlareSolverr — it no longer ships or launches its own browser. CloudflareInterceptor detects a challenge and asks the configured FlareSolverr to solve it automatically. On success it captures the cookies (including HttpOnly cf_clearance) and — because Cloudflare binds those cookies to a User-Agent — remembers the solver’s UA for that host. Subsequent requests fly through (~1 s). If FlareSolverr isn’t configured or fails, the API returns a clear error telling you to configure it.

Local persistence

Everything is stored in data/cache/miwayomi.db (SQLite, WAL mode):

StoreWhat it holds
kv_storeKey-value settings (including resolved hosts cf_ua_* and your repository URLs user_repos).
cookiesCloudflare and site cookies, persisted across restarts.
favoritesFavorites + last-read chapter per entry.
watch_historyAnime watch progress per episode (resume position, duration, episode number).
extensionsInstalled extensions (package, name, version, files, install time).

Source preferences live in data/prefs/source_<id>.properties, and the proxied-image cache in data/cache/images/.

Troubleshooting

SymptomCause & fix
Stub! on an android.* methodOnly a stub in android.jar; implement it in android-compat and regenerate the jar.
VerifyError: ... not assignable to ContextWrapperThe Context hierarchy is wrong; keep Application : ContextWrapper : Context.
Search returns HTTP 500 (VerifyError: Call to wrong <init> method)The extension jar was corrupted by DEX→JVM conversion. Use v0.2.5+ (auto-repairs jars on first load) or reinstall the extension; a repo-published JVM jar is used automatically.
Port already in use after rebuildOld process alive: pkill -9 -f "server/build/install/server/lib".
One package shows as dozens of sourcesA single APK can declare many mirrors; the UI groups them by package.
Source preferences don’t appearPreferenceScreen must alias androidx.preference.PreferenceScreen (it does in source-api).
Incremental build seems staleUse ./gradlew :server:clean :server:installDist.

Roadmap

Good first places to read the code: builtin/DemoSource.kt, extension/JarFixer.kt, and api/StreamingRoutes.kt.

Glossary

TermMeaning
ExtensionAn APK in the Tachiyomi/Aniyomi format that declares one or more sources.
SourceA catalog provider (manga or anime) implementing the source API.
FactorySourceFactory/AnimeSourceFactory; how one APK provides several sources.
ShimThe android-compat layer that makes extensions believe Android exists.
JVM jarA desktop JVM jar some repositories publish alongside the APK; loaded directly, skipping the DEX conversion.
JarFixerThe ASM pass that repairs the bytecode corruption DEX→JVM conversion leaves behind (invalid <init> owners), applied automatically on first load.
HLS / DASHAdaptive streaming formats (.m3u8 / .mpd) transparently proxied.
SQLite storeLocal persistence for cookies, favorites, watch history, installed extensions, and settings.

miwayomi is an execution engine, not a content service.