How to Extend Immich Functionality Using the Plugin System: A WASM Development Guide
Developers extend Immich functionality by compiling WebAssembly modules that expose filters and actions, describing them in a manifest.json file, and placing them in the configured plugin directory where PluginService automatically loads and executes them via the Extism runtime.
Immich supports custom functionality through a secure WebAssembly plugin system defined in server/src/services/plugin.service.ts. Developers write sandboxed WASM modules in languages like Rust or Go, define their capabilities in a manifest file, and register them as workflow filters or actions. This guide covers the complete architecture, from manifest validation to host function integration, based on the immich-app/immich source code.
Understanding the Immich Plugin Architecture
The plugin system follows a five-stage bootstrap process orchestrated by PluginService.onBootstrap. First, the service scans resourcePaths.corePlugin and external install folders for manifest.json files. Then readAndValidateManifest parses each manifest into a PluginManifestDto and validates it using class-validator.
Valid plugins are up-serted to the database via loadPluginToDatabase, including their filters and actions. Finally, loadPlugins creates Extism plugin instances for each compiled .wasm file, injecting host functions that bridge WASM code to Immich's API. When workflow triggers fire, handleTrigger queues jobs that execute the WASM functions with proper context and authentication.
Building a WASM Plugin for Immich
Step 1: Compile the WebAssembly Module
Create a Rust project (or use Go/AssemblyScript) and export functions matching Immich's expected signatures. For example, a filter that processes assets:
use extism::CurrentPlugin;
#[no_mangle]
pub extern "C" fn filterAsset(cp: CurrentPlugin, offset: i64) -> i32 {
let result = r#"{"passed": true}"#;
cp.set_result(offset, result);
0
}
Compile this to my-plugin.wasm. The function name filterAsset must match the manifest definition.
Step 2: Create the Plugin Manifest
Define a manifest.json that declares the plugin metadata, filters, actions, and required permissions:
{
"name": "my-plugin",
"title": "My Sample Plugin",
"description": "Demo filter and action",
"author": "Jane Doe",
"version": "0.1.0",
"wasmPath": "my-plugin.wasm",
"filters": [
{
"methodName": "filterAsset",
"title": "Only photos",
"description": "Passes only image assets",
"supportedContexts": ["Asset"],
"schema": null
}
],
"actions": [
{
"methodName": "actionAddTag",
"title": "Add tag",
"description": "Adds a tag to the asset",
"supportedContexts": ["Asset"],
"schema": {
"type": "object",
"properties": {
"tag": { "type": "string" }
},
"required": ["tag"]
}
}
]
}
The methodName fields must correspond to exported WASM functions. The schema field defines configuration UI using JSON Schema.
Step 3: Deploy to the Plugin Directory
Place the plugin folder containing both manifest.json and the .wasm file in either the core plugin path (resourcePaths.corePlugin) or the external install folder specified by configRepository.getEnv().plugins.external.installFolder. Immich scans these locations during PluginService.onBootstrap.
Calling Immich APIs from Plugins
WASM plugins cannot directly access the database. Instead, they invoke host functions exposed by PluginHostFunctions.getHostFunctions in server/src/services/plugin-host.functions.ts. Available operations include updateAsset and addAssetToAlbum.
When a workflow trigger fires, handleTrigger queues a WorkflowRun job. The job runner calls your WASM function with a PluginInput object:
interface PluginInput<T = unknown> {
authToken: string; // JWT for permission validation
config: T; // Workflow-specific configuration
data: { asset: Asset }; // Triggering asset data
}
To update an asset from Rust:
use extism::CurrentPlugin;
use serde_json::json;
#[no_mangle]
pub extern "C" fn actionAddTag(cp: CurrentPlugin, offset: i64) -> i32 {
let input: serde_json::Value = cp.read(offset).unwrap();
let auth = input["authToken"].as_str().unwrap();
let asset_id = input["data"]["asset"]["id"].as_str().unwrap();
let tag = input["config"]["tag"].as_str().unwrap();
let payload = json!({
"authToken": auth,
"id": asset_id,
"title": format!("{} {}", input["data"]["asset"]["title"], tag)
});
cp.call_host("extism:host/user", "updateAsset", &payload.to_string());
0
}
The host function validates the JWT token and enforces Immich's permission model before executing the update via assetRepository.update.
Workflow Integration and Execution
After deployment, create a workflow in the Immich UI or API that references your plugin's filter and action IDs. When an event like AssetCreate fires, handleAssetCreate calls handleTrigger, which eventually invokes handleWorkflowRun. The system loads the workflow definition, applies filters to determine if the action should run, and executes the WASM function with the configured parameters.
Plugins receive a fresh authentication token for each invocation, ensuring secure, short-lived access credentials.
Key Source Files for Plugin Development
server/src/services/plugin.service.ts: Core loader, manifest validation, and workflow integrationserver/src/services/plugin-host.functions.ts: Host function implementations (updateAsset,addAssetToAlbum)server/src/dtos/plugin-manifest.dto.ts: Validation schema formanifest.jsonserver/src/dtos/plugin.dto.ts: Data transfer objects for plugin metadataserver/src/enums/enum.ts:PluginTriggerTypeandPluginContextdefinitions
Summary
- Write WASM modules in Rust, Go, or AssemblyScript using the Extism SDK
- Define capabilities in
manifest.jsonwith filters, actions, and JSON schemas - Deploy to core or external plugin directories scanned by
PluginService.onBootstrap - Interact with Immich data only through injected host functions that validate JWT tokens
- Configure workflows in the UI to trigger your plugin on events like
AssetCreate
Frequently Asked Questions
What programming languages can I use to write Immich plugins?
You can use any language that compiles to WebAssembly and supports the Extism SDK. The immich-app/immich source code shows examples in Rust, but Go, AssemblyScript, C, and Zig are also compatible. The compiled output must be a .wasm file referenced by your manifest.json.
How does Immich secure plugin execution?
Immich sandboxes plugins using the Extism runtime and validates all API calls through host functions in PluginHostFunctions. Each plugin invocation receives a unique JWT authToken that the host functions verify before performing operations like updateAsset. This ensures plugins operate within Immich's permission model and cannot access unauthorized data.
Do I need to restart Immich to load new plugins?
The PluginService scans for plugins during bootstrap via onBootstrap. While a restart ensures loadPluginsFromManifests picks up new manifests, Immich also exposes a reload endpoint that triggers the loading sequence without a full restart. Place your plugin in the configured directory and either restart or hit the reload endpoint.
Can plugins modify any asset metadata?
Plugins can only modify assets through the specific host functions exposed in PluginHostFunctions, such as updateAsset or addAssetToAlbum. Direct database access is prohibited. The host functions enforce field-level permissions and validate the plugin's JWT token before applying changes, ensuring data integrity and security.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →