plugins/README.md
Navidrome supports WebAssembly (Wasm) plugins for extending functionality. Plugins run in a secure sandbox and can provide metadata agents, scrobblers, lyrics providers, audio similarity, and other integrations through host services like scheduling, caching, task queues, WebSockets, and Subsonic API access.
The plugin system is built on Extism, a cross-language framework for building WebAssembly plugins. You can write plugins in any language that Extism supports (Go, Rust, Python, TypeScript, and more) using their Plugin Development Kits (PDKs).
Essential Extism Resources:
Create main.go:
package main
import "github.com/extism/go-pdk"
func main() {}
// Implement your capability functions here
Create manifest.json:
{
"name": "My Plugin",
"author": "Your Name",
"version": "1.0.0"
}
# Compile to WebAssembly
tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared .
# Package as .ndp (zip archive)
zip -j my-plugin.ndp manifest.json plugin.wasm
Copy my-plugin.ndp to your Navidrome plugins folder and enable plugins in your config:
[Plugins]
Enabled = true
Folder = "/path/to/plugins"
A Navidrome plugin is an .ndp package file (zip archive) containing:
manifest.json – Plugin metadata (name, author, version, permissions)plugin.wasm – Compiled WebAssembly module with capability functionsPlugins are identified by their filename (without .ndp extension), not the manifest name field:
my-plugin.ndp → plugin ID is my-pluginname is the display name shown in the UIThis allows users to have multiple instances of the same plugin with different configs by renaming the files.
Every plugin must include a manifest.json file. Example:
{
"name": "My Plugin",
"author": "Author Name",
"version": "1.0.0",
"description": "What this plugin does",
"website": "https://example.com",
"config": {
"schema": { ... },
"uiSchema": { ... }
},
"permissions": {
"http": {
"reason": "Fetch metadata from external API",
"requiredHosts": ["api.example.com", "*.musicbrainz.org"]
}
}
}
Required fields: name, author, version
Optional fields: description, website, config, permissions, experimental
The config field defines the plugin's configuration schema using JSON Schema (draft-07) and an optional JSONForms UI schema for rendering in the Navidrome web UI:
{
"config": {
"schema": {
"type": "object",
"properties": {
"api_key": { "type": "string", "title": "API Key" },
"max_retries": { "type": "integer", "default": 3 }
},
"required": ["api_key"]
},
"uiSchema": {
"api_key": { "ui:widget": "password" }
}
}
}
Plugins can opt-in to experimental WebAssembly features that may change or be removed in future versions. Currently supported:
threads – Enables WebAssembly threads support (for plugins compiled with multi-threading){
"experimental": {
"threads": {
"reason": "Required for concurrent audio processing"
}
}
}
Note: Experimental features may have compatibility or performance implications. Use only when necessary.
Capabilities define what your plugin can do. They're automatically detected based on which functions you export. A plugin can implement multiple capabilities.
Provides artist and album metadata. All methods are optional — implement only the ones your data source supports.
| Function | Input | Output | Description |
|---|---|---|---|
nd_get_artist_mbid | {id, name} | {mbid} | Get MusicBrainz ID |
nd_get_artist_url | {id, name, mbid?} | {url} | Get artist URL |
nd_get_artist_biography | {id, name, mbid?} | {biography} | Get artist biography |
nd_get_similar_artists | {id, name, mbid?, limit} | {artists: [{name, mbid?}]} | Get similar artists |
nd_get_artist_images | {id, name, mbid?} | {images: [{url, size}]} | Get artist images |
nd_get_artist_top_songs | {id, name, mbid?, count} | {songs: [{name, mbid?}]} | Get top songs |
nd_get_album_info | {name, artist, mbid?} | {name, mbid, description, url} | Get album info |
nd_get_album_images | {name, artist, mbid?} | {images: [{url, size}]} | Get album images |
nd_get_similar_songs_by_track | {id, name, artist, ...} | {songs: [{name, artist}]} | Similar songs by track |
nd_get_similar_songs_by_album | {id, name, artist, ...} | {songs: [{name, artist}]} | Similar songs by album |
nd_get_similar_songs_by_artist | {id, name, mbid?, count} | {songs: [{name, artist}]} | Similar songs by artist |
To use the plugin as a metadata agent, add it to your config:
Agents = "lastfm,spotify,my-plugin"
Example (using Go PDK package):
package main
import "github.com/navidrome/navidrome/plugins/pdk/go/metadata"
type myPlugin struct{}
func (p *myPlugin) GetArtistBiography(input metadata.ArtistRequest) (*metadata.ArtistBiographyResponse, error) {
return &metadata.ArtistBiographyResponse{Biography: "Biography text..."}, nil
}
func init() { metadata.Register(&myPlugin{}) }
func main() {}
Example (raw wasmexport):
//go:wasmexport nd_get_artist_biography
func ndGetArtistBiography() int32 {
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
pdk.OutputJSON(BiographyOutput{Biography: "Artist biography..."})
return 0
}
Integrates with external scrobbling services. All three methods are required.
| Function | Input | Output | Description |
|---|---|---|---|
nd_scrobbler_is_authorized | {username} | bool | Check if user is authorized |
nd_scrobbler_now_playing | See below | (none) | Send now playing |
nd_scrobbler_scrobble | See below | (none) | Submit a scrobble |
Important: Scrobbler plugins require the
userspermission in their manifest. Scrobble events are only sent for users assigned to the plugin through Navidrome's configuration.
Manifest permission:
{
"permissions": {
"users": {
"reason": "Receive scrobble events for users assigned to this plugin"
}
}
}
NowPlaying/Scrobble Input:
{
"username": "john",
"track": {
"id": "track-id",
"title": "Song Title",
"album": "Album Name",
"artist": "Artist Name",
"albumArtist": "Album Artist",
"duration": 180.5,
"trackNumber": 1,
"discNumber": 1,
"mbzRecordingId": "...",
"mbzAlbumId": "...",
"mbzArtistId": "..."
},
"timestamp": 1703270400
}
Error Handling:
On success, return 0. On failure, use pdk.SetError() with one of these error types:
scrobbler(not_authorized) – User needs to re-authorizescrobbler(retry_later) – Temporary failure, Navidrome will retryscrobbler(unrecoverable) – Permanent failure, scrobble discardedimport "github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
return scrobbler.ScrobblerErrorNotAuthorized
return scrobbler.ScrobblerErrorRetryLater
return scrobbler.ScrobblerErrorUnrecoverable
Provides lyrics for tracks. The single method is required.
| Function | Input | Output | Description |
|---|---|---|---|
nd_lyrics_get_lyrics | {artistName, title, ...} | {lyrics: [{lang, text}]} | Get lyrics |
Each returned lyric entry has a lang (language code) and text field. Multiple entries can be returned for different languages.
Audio-similarity discovery based on acoustic features (e.g., embeddings). Both methods are required.
| Function | Input | Output | Description |
|---|---|---|---|
nd_get_sonic_similar_tracks | {song, count} | {matches: [{song, similarity}]} | Find acoustically similar tracks |
nd_find_sonic_path | {startSong, endSong, count} | {matches: [{song, similarity}]} | Find a path between two songs |
Each match contains a song reference and a similarity score (float64, 0.0–1.0).
Processes tasks from a queue. The method is optional — export it if your plugin uses the Task host service for background work.
| Function | Input | Output | Description |
|---|---|---|---|
nd_task_execute | {queueName, taskID, payload, attempt} | string | Execute a queued task |
The payload is raw bytes (the same bytes passed to TaskEnqueue). The attempt counter starts at 1 and increments on retries. Return a string result on success.
Optional initialization callback. Called once after the plugin fully loads.
| Function | Input | Output | Description |
|---|---|---|---|
nd_on_init | {} | {error?} | Called once after plugin loads |
Useful for initializing connections, scheduling recurring tasks, etc. Errors are logged but don't prevent the plugin from loading.
Receives scheduled task events. Required if your plugin uses the Scheduler host service.
| Function | Input | Output | Description |
|---|---|---|---|
nd_scheduler_callback | {scheduleId, payload, isRecurring} | (none) | Handle scheduled task event |
Receives WebSocket events. Export any subset of these to handle events from the WebSocket host service.
| Function | Input | Description |
|---|---|---|
nd_websocket_on_text_message | {connectionId, message} | Text message received |
nd_websocket_on_binary_message | {connectionId, data} | Binary message received (base64) |
nd_websocket_on_error | {connectionId, error} | Connection error |
nd_websocket_on_close | {connectionId, code, reason} | Connection closed |
Host services let your plugin call back into Navidrome for advanced functionality. Each service (except Config) requires declaring the corresponding permission in your manifest.
All host service examples below use the generated Go SDK. Add this to your go.mod:
require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
Then import:
import "github.com/navidrome/navidrome/plugins/pdk/go/host"
Make HTTP requests to external services. This is a dedicated host service (separate from Extism's built-in HTTP support) with additional features like timeouts and redirect control.
Manifest permission:
{
"permissions": {
"http": {
"reason": "Fetch metadata from external API",
"requiredHosts": ["api.example.com", "*.musicbrainz.org"]
}
}
}
Host functions:
| Function | Parameters | Returns |
|---|---|---|
http_send | method, url, headers, body, timeoutMs, noFollowRedirects | statusCode, headers, body |
Usage:
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "GET",
URL: "https://api.example.com/data",
Headers: map[string]string{"Authorization": "Bearer " + apiKey},
})
if resp.StatusCode == 200 {
// Process resp.Body
}
Schedule one-time or recurring tasks. Your plugin must export the nd_scheduler_callback function to receive events.
Manifest permission:
{
"permissions": {
"scheduler": {
"reason": "Schedule periodic metadata refresh"
}
}
}
Host functions:
| Function | Parameters | Description |
|---|---|---|
scheduler_scheduleonetime | delaySeconds, payload, scheduleId? | Schedule one-time callback |
scheduler_schedulerecurring | cronExpression, payload, scheduleId? | Schedule recurring callback |
scheduler_cancelschedule | scheduleId | Cancel a scheduled task |
Usage:
// Schedule one-time task in 60 seconds
scheduleID, err := host.SchedulerScheduleOneTime(60, "my-payload", "")
// Schedule recurring task with cron expression (every hour)
scheduleID, err := host.SchedulerScheduleRecurring("0 * * * *", "hourly-task", "")
// Cancel a task
err := host.SchedulerCancelSchedule(scheduleID)
In-memory TTL-based cache. Each plugin has its own isolated namespace. Cleared on server restart.
Manifest permission:
{
"permissions": {
"cache": {
"reason": "Cache API responses to reduce external requests"
}
}
}
Host functions:
| Function | Parameters | Description |
|---|---|---|
cache_setstring | key, value, ttl_seconds | Store a string |
cache_getstring | key | Get a string |
cache_setint | key, value, ttl_seconds | Store an integer |
cache_getint | key | Get an integer |
cache_setfloat | key, value, ttl_seconds | Store a float |
cache_getfloat | key | Get a float |
cache_setbytes | key, value, ttl_seconds | Store bytes |
cache_getbytes | key | Get bytes |
cache_has | key | Check if key exists |
cache_remove | key | Delete a cached value |
TTL: Pass 0 for the default (24 hours), or specify seconds.
Usage:
// Cache a value for 1 hour
host.CacheSetString("api-response", responseData, 3600)
// Retrieve (returns value, exists, error)
value, exists, err := host.CacheGetString("api-response")
if exists {
// Use value
}
Persistent key-value storage backed by SQLite. Survives server restarts. Each plugin has its own isolated database at ${DataFolder}/plugins/${pluginID}/kvstore.db.
Manifest permission:
{
"permissions": {
"kvstore": {
"reason": "Store OAuth tokens and plugin state",
"maxSize": "1MB"
}
}
}
maxSize: Maximum storage size (e.g., "1MB", "500KB"). Default: 1MBKey constraints: Maximum 256 bytes, must be valid UTF-8.
Host functions:
| Function | Parameters | Description |
|---|---|---|
kvstore_set | key, value | Store a byte value |
kvstore_setwithttl | key, value, ttlSeconds | Store with auto-expiration |
kvstore_get | key | Retrieve a byte value |
kvstore_getmany | keys | Retrieve multiple values at once |
kvstore_has | key | Check if key exists |
kvstore_list | prefix | List keys matching prefix |
kvstore_delete | key | Delete a value |
kvstore_deletebyprefix | prefix | Delete all keys matching prefix |
kvstore_getstorageused | – | Get current storage usage (bytes) |
Usage:
// Store a value (as raw bytes)
token := []byte(`{"access_token": "xyz", "refresh_token": "abc"}`)
host.KVStoreSet("oauth:spotify", token)
// Store with TTL (auto-expires after 1 hour)
host.KVStoreSetWithTTL("session:abc", sessionData, 3600)
// Retrieve a value
value, exists, err := host.KVStoreGet("oauth:spotify")
if exists {
var tokenData map[string]string
json.Unmarshal(value, &tokenData)
}
// Batch retrieve
results, err := host.KVStoreGetMany([]string{"key1", "key2", "key3"})
// List and delete by prefix
keys, err := host.KVStoreList("user:")
host.KVStoreDeleteByPrefix("user:")
// Check storage usage
usage, err := host.KVStoreGetStorageUsed()
fmt.Printf("Using %d bytes\n", usage)
Background task queue with retry support. Plugins enqueue tasks and process them by exporting the nd_task_execute capability function.
Manifest permission:
{
"permissions": {
"taskqueue": {
"reason": "Process audio analysis in the background",
"maxConcurrency": 2
}
}
}
Host functions:
| Function | Parameters | Description |
|---|---|---|
task_createqueue | name, concurrency, maxRetries, backoffMs, ... | Create a named task queue |
task_enqueue | queueName, payload | Add a task to the queue |
task_get | taskID | Get task status and result |
task_cancel | taskID | Cancel a pending task |
task_clearqueue | queueName | Remove all tasks from queue |
Usage:
// Create a queue with retry configuration
host.TaskCreateQueue("analysis", host.QueueConfig{
Concurrency: 2,
MaxRetries: 3,
BackoffMs: 1000,
})
// Enqueue a task
taskID, err := host.TaskEnqueue("analysis", []byte(`{"trackId": "abc"}`))
// Check task status
info, err := host.TaskGet(taskID)
fmt.Printf("Status: %s, Attempt: %d\n", info.Status, info.Attempt)
Establish persistent WebSocket connections to external services. Your plugin must export WebSocketCallback functions to receive events.
Manifest permission:
{
"permissions": {
"websocket": {
"reason": "Real-time connection to service",
"requiredHosts": ["gateway.example.com", "*.discord.gg"]
}
}
}
Host functions:
| Function | Parameters | Description |
|---|---|---|
websocket_connect | url, headers?, connectionId? | Open a connection |
websocket_sendtext | connectionId, message | Send text message |
websocket_sendbinary | connectionId, data | Send binary data |
websocket_closeconnection | connectionId, code?, reason? | Close connection |
Usage:
connID, err := host.WebSocketConnect("wss://gateway.example.com", nil, "")
host.WebSocketSendText(connID, `{"op": 1, "d": null}`)
host.WebSocketCloseConnection(connID, 1000, "done")
Access music library metadata and optionally read files from library directories.
Manifest permission:
{
"permissions": {
"library": {
"reason": "Access library metadata for analysis",
"filesystem": false
}
}
}
filesystem – Set to true to enable read-only access to library directories (default: false)Host functions:
| Function | Parameters | Returns |
|---|---|---|
library_getlibrary | id | Library metadata |
library_getalllibraries | (none) | Array of library metadata |
Library metadata:
{
"id": 1,
"name": "My Music",
"path": "/music/collection",
"mountPoint": "/libraries/1",
"lastScanAt": 1703270400,
"totalSongs": 5000,
"totalAlbums": 500,
"totalArtists": 200,
"totalSize": 50000000000,
"totalDuration": 1500000.5
}
Note: The
pathandmountPointfields are only included whenfilesystem: trueis set in the permission.
Filesystem access:
When filesystem: true, your plugin can read files from library directories via WASI filesystem APIs. Each library is mounted at /libraries/<id>:
import "os"
content, err := os.ReadFile("/libraries/1/Artist/Album/track.mp3")
entries, err := os.ReadDir("/libraries/1/Artist")
Security: Filesystem access is read-only and restricted to configured library paths only.
Usage:
// Get a specific library
library, err := host.LibraryGetLibrary(1)
fmt.Printf("Library: %s (%d songs)\n", library.Name, library.TotalSongs)
// Get all libraries
libraries, err := host.LibraryGetAllLibraries()
for _, lib := range libraries {
fmt.Printf("Library: %s (%d songs)\n", lib.Name, lib.TotalSongs)
}
Generate public URLs for Navidrome artwork (albums, artists, tracks, playlists).
Manifest permission:
{
"permissions": {
"artwork": {
"reason": "Get artwork URLs for display"
}
}
}
Host functions:
| Function | Parameters | Returns |
|---|---|---|
artwork_getartisturl | id, size | Artwork URL |
artwork_getalbumurl | id, size | Artwork URL |
artwork_gettrackurl | id, size | Artwork URL |
artwork_getplaylisturl | id, size | Artwork URL |
Usage:
url, err := host.ArtworkGetAlbumUrl("album-id", 300)
Call Navidrome's Subsonic API internally (no network round-trip).
Manifest permission:
{
"permissions": {
"subsonicapi": {
"reason": "Access library data"
},
"users": {
"reason": "Access user information for SubsonicAPI authorization"
}
}
}
Important: The
subsonicapipermission requires theuserspermission. Which users the plugin can act as is controlled through the Navidrome UI.
Host functions:
| Function | Parameters | Returns |
|---|---|---|
subsonicapi_call | uri | JSON response string |
subsonicapi_callraw | uri | Content type + binary response |
Usage:
// JSON response
response, err := host.SubsonicAPICall("getAlbumList2?type=random&size=10&u=username")
// Binary response (e.g., cover art, streams)
contentType, data, err := host.SubsonicAPICallRaw("getCoverArt?id=al-123&u=username")
Access plugin configuration values. Unlike pdk.GetConfig() which only retrieves individual values, this service can list all available configuration keys — useful for discovering dynamic configuration.
Note: This service is always available and does not require a manifest permission.
Host functions:
| Function | Parameters | Returns |
|---|---|---|
config_get | key | value, exists |
config_getint | key | value, exists |
config_keys | prefix | Array of matching key names |
Usage:
// Get a configuration value
value, exists := host.ConfigGet("api_key")
// Get an integer configuration value
count, exists := host.ConfigGetInt("max_retries")
// List all keys with a prefix (useful for user-specific config)
keys := host.ConfigKeys("user:")
// List all configuration keys
allKeys := host.ConfigKeys("")
Access user information for the users that the plugin has been granted access to.
Manifest permission:
{
"permissions": {
"users": {
"reason": "Display user information in status updates"
}
}
}
Important: Before enabling a plugin that requires the users permission, an administrator must configure which users the plugin can access:
If neither option is configured, the plugin cannot be enabled.
Host functions:
| Function | Parameters | Returns |
|---|---|---|
users_getusers | – | Array of User objects |
users_getadmins | – | Array of admin Users |
User object fields:
| Field | Type | Description |
|---|---|---|
userName | string | The user's unique username |
name | string | The user's display name |
isAdmin | boolean | Whether the user is an admin |
Security: Sensitive fields like passwords, email addresses, and internal IDs are never exposed to plugins.
Usage:
users, err := host.UsersGetUsers()
for _, user := range users {
pdk.Log(pdk.LogInfo, "User: " + user.UserName + " (" + user.Name + ")")
}
admins, err := host.UsersGetAdmins()
Enable plugins in navidrome.toml:
[Plugins]
Enabled = true
Folder = "/path/to/plugins" # Default: DataFolder/plugins
AutoReload = true # Auto-reload on file changes (dev mode)
LogLevel = "debug" # Plugin-specific log level
CacheSize = "200MB" # Compilation cache size limit
Plugin configuration is managed through the Navidrome web UI. Navigate to the Plugins page, select a plugin, and edit its configuration as key-value pairs.
Access configuration values in your plugin:
apiKey, ok := pdk.GetConfig("api_key")
if !ok {
pdk.SetErrorString("api_key configuration is required")
return 1
}
For more advanced access (listing keys, integer values), use the Config host service.
Plugins can be written in any language that Extism supports. We recommend:
# Install TinyGo: https://tinygo.org/getting-started/install/
# Build WebAssembly module
tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared .
# Package as .ndp
zip -j my-plugin.ndp manifest.json plugin.wasm
Navidrome provides type-safe Go packages for each capability and host service in plugins/pdk/go/. Instead of manually exporting functions with //go:wasmexport, use the Register() pattern:
package main
import "github.com/navidrome/navidrome/plugins/pdk/go/metadata"
type myPlugin struct{}
func (p *myPlugin) GetArtistBiography(input metadata.ArtistRequest) (*metadata.ArtistBiographyResponse, error) {
return &metadata.ArtistBiographyResponse{Biography: "Biography text..."}, nil
}
func init() { metadata.Register(&myPlugin{}) }
func main() {}
Add to your go.mod:
require github.com/navidrome/navidrome v0.0.0
replace github.com/navidrome/navidrome => ../../..
Available capability packages:
| Package | Import Path | Description |
|---|---|---|
metadata | plugins/pdk/go/metadata | Artist/album metadata providers |
scrobbler | plugins/pdk/go/scrobbler | Scrobbling services |
lyrics | plugins/pdk/go/lyrics | Lyrics providers |
sonicsimilarity | plugins/pdk/go/sonicsimilarity | Audio similarity discovery |
taskworker | plugins/pdk/go/taskworker | Background task processing |
lifecycle | plugins/pdk/go/lifecycle | Plugin initialization |
scheduler | plugins/pdk/go/scheduler | Scheduled task callbacks |
websocket | plugins/pdk/go/websocket | WebSocket event handlers |
host | plugins/pdk/go/host | Host service SDK (all services) |
See the example plugins in examples/ for complete usage patterns.
# Build WebAssembly module
cargo build --release --target wasm32-wasip1
# Package as .ndp
zip -j my-plugin.ndp manifest.json target/wasm32-wasip1/release/plugin.wasm
# Cargo.toml
[dependencies]
nd-pdk = { path = "../../pdk/rust/nd-pdk" }
extism-pdk = "1.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Implementing capabilities with traits and macros:
use nd_pdk::scrobbler::{Scrobbler, IsAuthorizedRequest, Error};
use nd_pdk::register_scrobbler;
#[derive(Default)]
struct MyPlugin;
impl Scrobbler for MyPlugin {
fn is_authorized(&self, req: IsAuthorizedRequest) -> Result<bool, Error> {
Ok(true)
}
fn now_playing(&self, req: NowPlayingRequest) -> Result<(), Error> { Ok(()) }
fn scrobble(&self, req: ScrobbleRequest) -> Result<(), Error> { Ok(()) }
}
register_scrobbler!(MyPlugin); // Generates all WASM exports
Using host services:
use nd_pdk::host::{cache, scheduler, library};
cache::set_string("my_key", "my_value", 3600)?;
scheduler::schedule_recurring("@every 5m", "payload", "task_id")?;
let libs = library::get_all_libraries()?;
See pdk/rust/README.md for detailed documentation.
# Build WebAssembly module (requires extism-py installed)
extism-py plugin.wasm -o plugin.wasm *.py
# Package as .ndp
zip -j my-plugin.ndp manifest.json plugin.wasm
For Python host services: Copy functions from the nd_host_*.py files in plugins/pdk/python/host/ into your __init__.py (see comments in those files for extism-py limitations).
Bootstrap a new plugin from a schema:
# Install XTP CLI: https://docs.xtp.dylibso.com/docs/cli
# Create a metadata agent plugin
xtp plugin init \
--schema-file plugins/capabilities/metadata_agent.yaml \
--template go \
--path ./my-agent \
--name my-agent
# Build and package
cd my-agent && xtp plugin build
zip -j my-agent.ndp manifest.json dist/plugin.wasm
See capabilities/README.md for available schemas and scaffolding examples.
See examples/ for complete working plugins:
| Plugin | Language | Capabilities | Host Services | Description |
|---|---|---|---|---|
| minimal | Go | MetadataAgent | – | Basic structure example |
| wikimedia | Go | MetadataAgent | HTTP | Wikidata/Wikipedia integration |
| coverartarchive-py | Python | MetadataAgent | HTTP | Cover Art Archive |
| coverartarchive-as | AssemblyScript | MetadataAgent | HTTP | Cover Art Archive |
| webhook-rs | Rust | Scrobbler | HTTP | HTTP webhooks |
| nowplaying-py | Python | Lifecycle | Scheduler, SubsonicAPI | Periodic now-playing logger |
| library-inspector-rs | Rust | Lifecycle | Library, Scheduler | Periodic library stats logging |
| crypto-ticker | Go | Lifecycle | WebSocket, Scheduler | Real-time crypto prices demo |
| discord-rich-presence-rs | Rust | Scrobbler | HTTP, WebSocket, Cache, Scheduler, Artwork | Discord integration |
Plugins run in a secure WebAssembly sandbox provided by Extism and the Wazero runtime:
library.filesystem permissionsubsonicapi or scrobbler capabilities can only access/receive events for users assigned to them through Navidrome's configurationWith AutoReload = true, Navidrome watches the plugins folder and automatically detects when .ndp files are added, modified, or removed. When a plugin file changes, the plugin is disabled and its metadata is re-read from the archive.
If AutoReload is disabled, Navidrome needs to be restarted to pick up plugin changes.
Plugins can be enabled/disabled via the Navidrome UI. The plugin state is persisted in the database.