This fork can load external plugin JARs at runtime, in addition to the
plugins compiled into Komga. You write a small Kotlin project, run
./gradlew build, and drop the resulting JAR into Komga.
There are two kinds of plugin today:
| Type | Interface | What it does |
|---|---|---|
METADATA |
MetadataProviderPlugin |
Search an online source and return series metadata. Shows up in “Search Online Databases”. |
NOTIFIER |
NotifierPlugin |
React to events (DOWNLOAD_COMPLETED, DOWNLOAD_FAILED) — webhook, Discord, log, … |
ANALYZER/PROCESSORinterfaces exist in the SPI but have no internal call-sites yet.DOWNLOADis intentionally not pluggable (it is tied to gallery-dl).
A ready-to-build template lives in plugins/plugin-template/. It
compiles standalone — you do not need the Komga JAR on your classpath.
cp -r plugins/plugin-template my-plugin
cd my-plugin
# 1. edit the code in src/main/kotlin/com/example/myplugin/
# 2. keep src/main/resources/META-INF/services/...KomgaPlugin in sync
./gradlew build
# -> build/libs/example-komga-plugin-1.0.0.jar
Then install it (either way works):
.jar (or paste a URL).<komga-config-dir>/plugins/ and restart.
(<komga-config-dir> is your KOMGA_CONFIGDIR, e.g. ~/.komga or the mounted
/config in Docker.)Uninstall from the Plugin Manager — it unloads the class loader and deletes the JAR.
Plugins run as admin-only installs and execute arbitrary code inside the Komga JVM. There is no sandbox. Only install plugins you trust.
<config-dir>/plugins/*.jar.URLClassLoader whose parent is a filtering
SpiOnlyClassLoader. The plugin sees ONLY:
org.gotson.komga.infrastructure.plugin.api.* — the SPIjava.*, javax.* — the JDKkotlin.*, kotlinx.* — Kotlin runtimecom.fasterxml.jackson.* — JSON parsing
Every other Komga-internal class (org.gotson.komga.domain.*,
org.gotson.komga.infrastructure.* outside plugin.api, Spring, SLF4J,
etc.) throws ClassNotFoundException with an explicit “Plugin denied
access” message. This is class-loader isolation, not a security
sandbox — see Security model below.ServiceLoader, which reads
META-INF/services/org.gotson.komga.infrastructure.plugin.api.KomgaPlugin.
Every plugin class you ship must be listed there, one fully-qualified
name per line.plugin table (your enabled toggle
survives restarts) and initialize(context) is called.Because the SPI classes come from Komga’s parent class loader, your JAR must
not contain its own copy of them — the template’s build.gradle.kts
excludes the org.gotson.komga.infrastructure.plugin.api package for that reason.
Anything outside the allow-list above must be bundled into your plugin JAR
(implementation(...) in your build.gradle.kts). The URLClassLoader serves
those classes from your JAR after the parent rejects them, so an OkHttp or
SQLite-JDBC dependency works fine if you ship it.
SpiOnlyClassLoader blocks compile-time / runtime imports of Komga internals
so a plugin can’t import org.gotson.komga.domain.service.SeriesLifecycle and
reach into the host. It is defense in depth, not a sandbox:
java.lang.reflect is on the allow-list (the JDK needs it for ServiceLoader)
so a plugin handed an object via the SPI can still reflect on it.java.io.File, java.net.http are intentionally allowed — plugins call
external APIs. Plugins have the same filesystem and network rights as the
Komga process.Built-in plugins (plugins/{anilist,kitsu,metron}-plugin/) are auditable in
this repo. Treat any external JAR as you would any program you’d run as the
Komga user. Real isolation (SecurityManager — deprecated/removed in Java 24,
JPMS, out-of-process plugins) is not provided.
interface KomgaPlugin {
val id: String // unique, stable, e.g. "anidb-metadata"
val name: String
val version: String
val author: String? // optional
val description: String? // optional
val type: KomgaPluginType // set by the sub-interface you implement
fun initialize(context: PluginContext) {}
fun shutdown() {}
}
interface MetadataProviderPlugin : KomgaPlugin {
fun search(query: String): List<PluginSearchResult>
fun getMetadata(externalId: String): PluginMetadataDetails?
}
interface NotifierPlugin : KomgaPlugin {
fun onNotification(notification: PluginNotification) // type/title/message/data
}
interface PluginContext {
val config: Map<String, String> // values stored for this plugin
fun info(message: String)
fun warn(message: String)
fun error(message: String, throwable: Throwable? = null)
}
DTOs (PluginSearchResult, PluginMetadataDetails, PluginAuthor,
PluginNotification) are plain data classes — see the mirror in the template or
the source at
komga/src/main/kotlin/org/gotson/komga/infrastructure/plugin/api/KomgaPlugin.kt.
Use the PluginContext you receive in initialize for logging (it writes to the
plugin’s log, visible in the Plugin Manager) and to read configuration values.
Anything you add as implementation(...) in build.gradle.kts is packaged into
your JAR and available at runtime. The Kotlin stdlib is not bundled (Komga
provides it) — keep using the same Kotlin version as Komga (2.2.x) to avoid
class-version surprises.
If you pull in large libraries, prefer a “fat JAR” (e.g. the Shadow plugin) so
all transitive dependencies end up inside the single JAR you install — but still
exclude the ...plugin.api package.
id, sensible name/version.MetadataProviderPlugin and/or NotifierPlugin.META-INF/services/org.gotson.komga.infrastructure.plugin.api.KomgaPlugin../gradlew build produces a JAR under build/libs/.<config-dir>/plugins/.