Host API Reference
The contract Mydia plugins run against: the event envelope and catalog, the capability classes and their host functions, the scheduled-handler export, and the manifest fields that govern versioning.
New to plugins? Start with the tutorial. For task-oriented recipes, see the how-to guides. For why the platform is shaped this way (the sandbox, the capability model, the host-version floor), see The plugin model.
The event
The host delivers a typed Event record:
| Field | Type | Notes |
|---|---|---|
event |
String |
The event type, e.g. media_item.added. |
category, severity |
Option<String> |
Envelope classification. |
actor_type, actor_id |
Option<String> |
Who triggered it. |
resource_type, resource_id |
Option<String> |
What it concerns. |
metadata_json |
String |
A JSON object string of per-event metadata (and any operator config). "{}" when empty. |
The arbitrary per-event detail (and the operator's plugin settings) ride in
metadata_json as a JSON object, so the typed envelope stays stable while the
payload varies by event. Parse it with any JSON crate when you need it.
Event catalog
A plugin subscribes to events in its manifest; each must be in the catalog:
media_item.added,media_item.updated,media_item.removedmedia_file.importeddownload.completed,download.failedplayback.started,playback.progressed,playback.paused,playback.finished
The playback.* events carry an origin (player, sync:<provider>, or
plugin:<slug>) in metadata_json. The dispatcher never delivers an event back
to the plugin that originated it, so write-backs don't echo. playback.progressed
is sampled (one per 5% bucket); playback.paused is reserved but not yet emitted.
Capabilities
Capabilities are deny-by-default and enforced server-side on every call. A plugin can never widen its own grant. A manifest declares what it wants; the operator approves it.
| Class | Meaning |
|---|---|
events:subscribe |
The event types the plugin reacts to (from the catalog above). Required. |
net:http |
The exact hostnames the plugin may contact. No wildcards (a wildcard subdomain is an exfiltration channel). |
data:read |
Scoped read namespaces (media_item, playback_progress). The host returns a curated, read-only projection: never raw rows or secrets. |
surfaces:write |
Curated write surfaces. Vocabulary: playback:watched (mark items watched via ensure-watched). |
state:kv |
A per-plugin key/value store (@max_keys 256 keys, 64 KB per value) for watermarks, cursors, and dedupe sets. |
users:connections |
Per-user third-party connections: the host holds the token; the plugin gets identity + status only. Cross-user, consent-scoped. |
schedule:interval |
Run on-schedule on a fixed interval (manifest schedule, 5-minute floor). |
Host functions
Reach capabilities through the typed SDK bindings under
mydia_plugin_sdk::host:
use mydia_plugin_sdk::host;
use mydia_plugin_sdk::types::{DataRequest, OutboundRequest, ReadResult};
// data:read (a curated media-item projection).
if let Ok(ReadResult::MediaItem(item)) =
host::data_read(&DataRequest { namespace: "media_item".into(), id })
{
let _ = item.title;
}
// net:http (a gated outbound request). The host re-validates the URL host
// against your net:http allowlist and runs an SSRF gate on every call.
let resp = host::http_request(&OutboundRequest {
url: "https://example.com/hook".into(),
method: "POST".into(),
headers: vec![("content-type".into(), "application/json".into())],
body: Some("{}".into()),
});
// log (ungated diagnostics into the plugin's activity log).
host::log("info", "did the thing");
Each result<_, host-error> surfaces a denial (Denied), a bad request, a
not-found, or a network error: handle it; the host never lets a guest bypass
the gate.
1.1 host functions
use mydia_plugin_sdk::host;
use mydia_plugin_sdk::types::{ListRequest, ListItem, WatchTarget};
// state:kv (opaque per-plugin storage across invocations).
host::kv_set("watermark", "2024-06-01T00:00:00Z").ok();
let mark = host::kv_get("watermark").ok().flatten(); // Option<String>
host::kv_delete("watermark").ok();
// data:read via data-list (cursor-paginated, updated-since filtered). Walk
// next_cursor until None. playback_progress is consent-scoped to connected users.
let page = host::data_list(&ListRequest {
namespace: "playback_progress".into(),
cursor: None,
updated_since: mark.clone(),
limit: Some(200),
}).unwrap();
for item in page.items {
if let ListItem::PlaybackProgress(p) = item { let _ = p.watched; }
}
// surfaces:write (mark watched for a user, idempotently). Host-side external-id
// matching; the response says changed / already-watched / not-found.
host::ensure_watched(&WatchTarget {
user_id: "…".into(),
imdb_id: Some("tt100".into()),
tmdb_id: None, tvdb_id: None,
season_number: None, episode_number: None,
watched_at: None,
}).ok();
// users:connections: identity + status only (never a token).
for c in host::connections_list().unwrap() { let _ = (c.id, c.user_id, c.status); }
// connection-request (an authenticated request). The host verifies the
// connection belongs to you, strips any guest Authorization, and injects the
// bearer token itself. You never see the token.
// host::connection_request(&c.id, &outbound_request)
Key guarantees:
ensure-watchedis idempotent: re-marking a watched item reportsalready-watchedand emits no event.data-listcursors are opaque and request-local: walk them within one run, never persist them.kv-setis an engine-native upsert (last write wins); keys are opaque to the host. Keys underconn/<connection-id>/...are swept when that connection is removed.
Scheduled handler
Add on-schedule for periodic work (declare a schedule and the
schedule:interval capability in the manifest):
use mydia_plugin_sdk::types::{Event, ScheduleTick};
#[mydia_plugin_sdk::plugin(on_schedule = on_schedule)]
fn on_event(evt: Event) -> Result<String, String> { Ok("{}".into()) }
fn on_schedule(tick: ScheduleTick) -> Result<String, String> {
// tick.config_json carries the operator settings. Return a small JSON result;
// include "connections_invalid": ["<user-id>"] to flag users whose token
// the provider rejected (a 401). The host marks those connections errored.
Ok("{\"connections_invalid\":[]}".into())
}
A run that takes longer than one interval is fine: the next tick is skipped while it runs (non-reentrant), and your state must survive a wall-clock kill, so checkpoint progress to KV as you go.
Manifest
A plugin ships a JSON manifest declaring its identity, the events it subscribes to, the capabilities it wants, and any operator-editable settings. See Manifest & Settings for the full field reference and a complete worked example.
Host-version floor
min_host_version (optional, a semantic version) declares the lowest Mydia host
your plugin supports. Mydia refuses to activate a plugin whose floor exceeds the
running host with a clear requires mydia >= X message (the friendly wrapper
over wasmtime's hard link-time refusal). Omit it if you have no floor.
Evolving the contract
The WIT package version is the ABI version. The contract evolves
additively: new host functions, new records, new variant cases, and new
exports are added without touching existing types or signatures. A plugin built
against an older minor keeps working: the host detects each guest's contract
version from its bytes and serves the matching interface namespace and exports,
so a 1.0 guest's on-event still resolves against a 1.1 host. Only a removal
or a signature change bumps the major version. Target the lowest host you need
via min_host_version; a 1.1 guest sets "min_host_version": "1.1.0" so an
older host refuses it cleanly rather than failing to link.
Reference
- WIT contract:
native/mydia_plugin_sdk/wit/plugin.wit - SDK crate:
native/mydia_plugin_sdk - Starter:
native/mydia_plugin_sdk/examples/minimal - Reference plugin:
plugins/webhook_notifier - Sideload helper:
native/mydia_plugin_sdk/sideload.sh