Architecture

How the Zipline-based plugin runtime works - service binding, host APIs, entry point, and CoreAPI.

Spotube uses Cash App's Zipline to run your Kotlin/JS plugin in a sandboxed JavaScript runtime. Understanding the service binding model is essential before implementing any API.

Entry point

Every plugin exports a top-level main() function where services are wired together. The function name is configured in build.gradle.kts:

zipline {
    mainFunction.set("com.example.myplugin.main")
}

Inside main(), you connect to the Zipline runtime and register your services. See the js_plugin_example for a complete, working entry point.

Receiving host APIs

Host APIs are services Spotube provides to your plugin - HTTP client, persistent storage, and webview.

private val zipline by lazy { Zipline.get() }

fun main() {
    val httpClient = zipline.take<HttpClientAPI>(HttpClientAPI_SERVICE_NAME)
    val storage = zipline.take<PersistedStorageAPI>(PersistedStorageAPI_SERVICE_NAME)
}

Each API has a SERVICE_NAME constant (e.g. HttpClientAPI_SERVICE_NAME). For every capability you declare in plugin.json, call take with the corresponding service.

Registering plugin APIs

Plugin APIs are services you provide to Spotube - metadata, audio, lyrics, scrobble. Each gets its own implementation class, registered via bind():

zipline.bind<CoreAPI>(CoreAPI_SERVICE_NAME, RealCoreAPI())
zipline.bind<MetadataSearchAPI>(MetadataSearchAPI_SERVICE_NAME, RealMetadataSearchAPI())

Only bind the APIs your plugin actually implements. Each unused API should simply be omitted - Spotube handles missing services gracefully.

Sharing dependencies

Since each API implementation is a separate class, wrap host APIs and pass them through constructors. Always create a SpotrClient from the raw HttpClientAPI - never pass the host API directly:

fun main() {
    val httpClient = zipline.take<HttpClientAPI>(HttpClientAPI_SERVICE_NAME)
    val storage = zipline.take<PersistedStorageAPI>(PersistedStorageAPI_SERVICE_NAME)

    val spotr = SpotrClient(httpClient) {
        defaultUrl = "https://api.example.com"
    }

    zipline.bind<CoreAPI>(CoreAPI_SERVICE_NAME, RealCoreAPI(storage))
    zipline.bind<MetadataSearchAPI>(MetadataSearchAPI_SERVICE_NAME, RealMetadataSearchAPI(spotr))
}

CoreAPI - required

Every plugin must implement CoreAPI. It handles plugin identity, authentication, and update checks. The whole source is available in the example's RealCoreAPI.

Member Purpose
requiresAuthentication Show login button in UI
loggedInFlow StateFlow<Boolean> - emit true when authenticated
login() Called on login tap - open WebView for OAuth, store tokens
logout() Called on logout - clear stored credentials
checkPluginUpdates(currentVersion) Return PluginUpdateInfo if update available, null otherwise
supportMarkdownText(currentVersion) Support/contact info in Markdown

Authentication flow

When requiresAuthentication is true, Spotube shows a login button. When tapped, login() is called. The typical pattern: open a WebView for the provider's OAuth page, watch the URL for the redirect callback, extract the auth code, exchange for tokens, store them, update loggedInFlow, and close the WebView.

See the full auth flow example in js_plugin_example for the complete implementation.

Service name constants

Every interface has a companion _SERVICE_NAME constant. Import them alongside the interface:

import dev.krtirtho.plugin_interfaces.plugin_apis.core.CoreAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.core.CoreAPI_SERVICE_NAME

The full list of available service constants is imported in the example's main.kt.