Files
wanderer/plugins/README.md
slothful-vassal 3b8f00fd26 feat: Refine trail category model (#1059)
* feat: advanced trail categories

* rename remote_category

* fix merge issues

* subcategories for plugins mapping

* fix refresh

* remove vertical trail filter category scrolling

* cleanup

* add confirm modal for disabling a category

* Fix federation issues

* fix review findings

* redesign category settings page

* remove prio badge

* fix subcategory badge layout

* optimize subcategory settings layout

* further settings page layout optimization

* fix subcategory icon position

* fix

* update docs

---------

Co-authored-by: Christian Beutel <>
2026-06-29 03:16:26 +02:00

17 KiB

wanderer plugins

This directory contains first-party WASM provider plugins.

Each plugin is a standalone Go/TinyGo module with:

  • plugin.json as the source manifest
  • plugins/schema/plugin.schema.json for editor completion and manifest help
  • go run github.com/open-wanderer/wanderer/plugins/sdk/cmd/manifestcheck for normalized dist manifest output
  • ignored dist/<plugin-id>/plugin.json and dist/<plugin-id>/plugin.wasm build output for runtime discovery

Build the dist bundles before running from a fresh checkout:

make plugins-build

The runtime loads plugins from direct child directories of data/plugins, for example data/plugins/strava/plugin.json. To build and install the bundled plugins into that gitignored local runtime directory, run:

make plugins-install-local

To rebuild a single plugin, install TinyGo and run:

cd plugins/strava
make build

Repeat for hammerhead and komoot as needed.

Release builds create plugin bundle archives in CI. The database Docker image does not include plugins; users install release bundles into data/plugins.

Plugin authors can reference the manifest schema from a source manifest:

{
  "$schema": "../schema/plugin.schema.json",
  "manifestVersion": "1.0",
  "type": "trails"
}

Runtime flows

This section maps the main runtime flows for debugging and maintenance. The diagrams use readable step names instead of every exact function name, but they point at the backend paths involved when the host invokes plugin capabilities, host requests, OAuth, and trail sending. The code these flows reference lives in the core backend under db/ (PocketBase handlers, sync manager, host functions), not in this plugins/ directory.

Sync overview

flowchart TD
  subgraph Host[Host backend]
    Manual[Manual sync]
    Cron[Scheduled sync]
    Discover[Refresh plugin cache]
    LoadPlugin[Load plugin]
    Instance[Plugin instance]
    Actor[Find actor]
    Auth[Refresh auth]
    Config[Resolve config]
    Session[Open WASM session]
    Dedupe[Skip known trails]
    Import[Import trail]
    Records[(trails waypoints photos)]
    Merge{Auto-merge?}
    AutoMerge[Try auto-merge]
    Done[Update sync status]
  end

  subgraph Plugin[WASM plugin]
    ListExport([List provider trails])
    DetailExport([Get trail details])
    Summaries[/Trail summaries/]
    TrailImport[/Trail import payload/]
  end

  Cron --> Discover
  Manual --> Discover
  Discover --> LoadPlugin
  LoadPlugin --> Instance
  Instance --> Actor
  Instance --> Auth
  Instance --> Config
  Actor --> Session
  Auth --> Session
  Config --> Session
  Session --> ListExport
  ListExport --> Summaries
  Summaries --> Dedupe
  Dedupe --> DetailExport
  DetailExport --> TrailImport
  TrailImport --> Import
  Import --> Records
  Records --> Merge
  Merge -->|yes| AutoMerge
  Merge -->|no| Done
  AutoMerge --> Done

User vs actor IDs

Plugin sync starts from plugin_instances.user, the local wanderer user that owns the plugin instance. The importer keeps that user ID for user-scoped host decisions, but writes imported record ownership through the user's local ActivityPub actor.

ID Used for
plugin_instances.user Deduplicating provider imports for that user and applying user privacy defaults.
activitypub_actors.id found by user Writing trails.author, waypoints.author, and summit_logs.author.

Host request boundary

Plugins cannot open provider connections themselves. They send a request spec to the host; the host resolves the connector, enforces policy, injects allowed auth, executes the HTTP request, and returns a bounded response. Host request failures after request decoding are returned to the plugin as HostResponse.error with the provider_unavailable code.

Host request bodies may be JSON, application/x-www-form-urlencoded, or multipart, subject to the manifest upload limits and content-type allow-list. Here "uploads" means plugin-to-provider request bodies, including login forms, not only media/file uploads. Redirect following is enabled by default; plugins can set followRedirects to false to receive a 3xx response directly and handle provider login flows step-by-step. HostResponse.headerValues preserves all values for headers such as Set-Cookie and is the only response-header representation exposed to plugins.

Plugins can emit host-visible diagnostics through the wanderer:log host function. The payload is a JSON object with a strict level (debug, info, warn, or error) and a non-empty message. The Go SDK exposes this as sdk.LogDebug, sdk.LogInfo, sdk.LogWarn, and sdk.LogError. Log messages are written to the host logs. Keep them short and never include secrets, credentials, cookies, tokens, authorization codes, or full URLs with query parameters.

sdk.LogInfo("provider detail fetch took 420ms externalID=abc")
sdk.LogWarn("provider returned an optional photo without a URL")

Declare host functions used by a capability in requiredHostFunctions, for example ["http_request", "log"].

sequenceDiagram
  box WASM plugin
    participant Plugin as Plugin code
  end
  box Plugin worker
    participant Worker as http_request host function
  end
  box Host backend
    participant Host as Host HTTP executor
  end
  box Provider API
    participant Provider as Provider API
  end

  Plugin->>Worker: HostRequestSpec
  Worker->>Host: http_request RPC
  Host->>Host: Resolve connector
  Host->>Host: Validate manifest policy
  alt denied
    Host-->>Worker: HostResponse.error provider_unavailable
    Worker-->>Plugin: HostResponse.error
  else allowed
    Host->>Host: Inject auth and apply limits
    Host->>Provider: Scoped HTTP request
    Provider-->>Host: HTTP response
    Host->>Host: Validate response
    Host-->>Worker: HostResponse
    Worker-->>Plugin: HostResponse
  end

Plugin discovery

Used when the backend refreshes the list of plugin bundles installed on disk and caches their manifests in PocketBase.

flowchart TD
  subgraph Host[Host backend]
    Refresh[Refresh plugin cache]
    Scan[Scan data/plugins]
    Load[Load bundle]
    Validate[Validate manifest]
    Store[(installed_plugins)]
  end

  subgraph Disk[Plugin directory]
    Bundle[(Plugin bundle)]
  end

  Refresh --> Scan
  Scan --> Bundle
  Bundle --> Load
  Load --> Validate
  Validate --> Store

Manifest configSchema defines plugin-owned settings that are passed to plugin exports. Host-owned settings are documented by the host and are not passed to plugins. A manifest may only suggest host defaults via hostConfig; the current host fields are:

Field Purpose
planned Enables list_routes.v1 sync.
completed Enables list_activities.v1 sync.
privacy Chooses provider visibility or local user privacy settings.
merge.available Controls whether the UI offers auto-merge for this plugin. Defaults to true.
merge.enabled Runs auto-merge after trail import.
createSummitLogForCompleted Creates summit logs for completed imports.
categoryMapping Maps metadata.providerCategory to local category or subcategory targets.
connectors Provides host-owned base URL, TLS, private-network, and storage redirect settings for configured connectors.

The settings UI lets users edit categoryMapping per plugin instance for trail import plugins. Mapping values can be strings for broad category-only compatibility, or objects with category and optional subcategory, using local record IDs or canonical names:

{
  "categoryMapping": {
    "Ride": { "category": "Biking", "subcategory": "Road" },
    "GravelRide": { "category": "Biking", "subcategory": "Gravel" },
    "Hike": "Hiking"
  }
}

Plugins may describe provider-owned category values for the settings UI with metadata.providerCategories. This is display-only metadata; categoryMapping keys still use the raw provider category values emitted as metadata.providerCategory.

Trail import plugins should keep provider-specific category values in metadata.providerCategory. They may also provide provider summary metrics in metadata.distance, metadata.elevationGain, metadata.elevationLoss, and metadata.duration; the host uses those positive values instead of GPX-derived summary metrics and falls back to GPX when a value is missing. Plugins may provide an intended start coordinate in metadata.providerStart as { "lat": 47.123, "lon": 8.456 }; the host uses it only when it is close enough to the imported GPX track to be plausible.

Photo descriptors may be returned either on the imported trail or on individual waypoints. The host downloads those media files and stores them on the corresponding PocketBase records.

List plugins

Used by the settings UI to show locally available plugins, their metadata, icons, capabilities, and current availability status.

Plugins may provide optional UI metadata through manifest.metadata:

Field Purpose
displayName Human-facing provider name shown in the UI. Falls back to manifest name.
displayNames Optional localized provider names keyed by locale, e.g. de or de-CH. Falls back to displayName and name.
descriptions Optional localized plugin descriptions keyed by locale. Falls back to manifest description.
providerCategories Optional metadata for provider-owned category values. The settings UI uses providerCategories.*.labels for localized category mapping labels.
icons.light Light-theme icon path inside the plugin bundle.
icons.dark Dark-theme icon path inside the plugin bundle.

Config schema fields may also localize plugin-owned UI text. The simple label and description strings remain valid fallbacks; optional labels and descriptions maps override them for matching locales. Select options can use label and labels in the same way. Fields with "required": true are validated in the settings modal. Fields with "hidden": true are not rendered in the settings modal, but their saved values are preserved and still passed to plugin exports.

Locale lookup uses the exact locale first, then the language, then en, then the simple fallback string.

{
  "description": "Imports public hike suggestions from Schweizer Wanderwege.",
  "metadata": {
    "displayName": "Schweizer Wanderwege",
    "displayNames": {
      "de": "Schweizer Wanderwege",
      "en": "Swiss Hiking Trails"
    },
    "descriptions": {
      "de": "Importiert öffentliche Wandervorschläge der Schweizer Wanderwege.",
      "en": "Imports public hike suggestions from Swiss Hiking Trails."
    }
  },
  "configSchema": [
    {
      "key": "maxPhotos",
      "type": "text",
      "label": "Max photos",
      "labels": {
        "de": "Max. Fotos",
        "en": "Max photos"
      },
      "description": "Maximum photos to import per hike. Use 0 for none or -1 for all.",
      "descriptions": {
        "de": "Maximale Anzahl Fotos pro Wanderung. 0 importiert keine Fotos, -1 alle.",
        "en": "Maximum photos to import per hike. Use 0 for none or -1 for all."
      },
      "required": true
    }
  ]
}
flowchart TD
  subgraph UI[Settings UI]
    Request[GET /plugins]
    Response[/PluginInfo list/]
  end

  subgraph Host[Host backend]
    Handler[PluginSystemPluginsList]
    Refresh[Refresh plugin cache]
    Load[Load installed plugins]
    Icons[Attach icons]
  end

  Request --> Handler
  Handler --> Refresh
  Refresh --> Load
  Load --> Icons
  Icons --> Response

Save plugin instance

Used whenever a user creates or updates their personal plugin configuration. This path is where auth values are encrypted and default status is assigned.

flowchart TD
  subgraph UI[Settings UI]
    Save[Save plugin instance]
  end

  subgraph Host[Host backend]
    Hook[create/update hook]
    Manifest[Load manifest]
    Status[Set status]
    Secrets[Find secret fields]
    Encrypt[Encrypt auth]
    Instance[(plugin_instances)]
  end

  Save --> Hook
  Hook --> Manifest
  Manifest --> Status
  Manifest --> Secrets
  Secrets --> Encrypt
  Status --> Instance
  Encrypt --> Instance

OAuth connection

Used when the UI connects a plugin instance to an OAuth provider. Start and callback are separate HTTP endpoints, but together they form one browser redirect flow. The host exchanges the authorization code and stores tokens encrypted on the plugin instance.

sequenceDiagram
  box Settings UI
    participant UI as Settings UI
  end
  box Host backend
    participant Start as OAuth start handler
    participant DB as plugin_instances
    participant Callback as OAuth callback handler
  end
  box OAuth provider
    participant Provider as OAuth provider
  end

  UI->>Start: Start OAuth
  Start->>Start: Load plugin and OAuth context
  Start->>Start: Decrypt auth and validate redirect
  Start->>DB: Store state and PKCE verifier
  Start-->>UI: Authorization URL
  UI->>Provider: Browser redirect
  Provider-->>Callback: Redirect with code
  Callback->>Callback: Load plugin
  Callback->>DB: Load encrypted auth and OAuth state
  Callback->>Provider: Exchange code at token endpoint
  Provider-->>Callback: Access and refresh tokens
  Callback->>DB: Store tokens encrypted
  Callback->>DB: Clear transient OAuth fields

Cron sync

Used by the scheduled background sync. It refreshes installed plugin metadata and syncs enabled plugin instances.

flowchart TD
  subgraph Host[Host backend]
    Cron[Scheduled sync]
    Refresh[Refresh plugin cache]
    Load[Load plugins]
    Instances[Enabled instances]
    Sync[Sync instance]
    Next[Next instance]
  end

  Cron --> Refresh
  Refresh --> Load
  Load --> Instances
  Instances --> Sync
  Sync --> Next
  Next --> Instances

Sync retry handling

Used when a previous sync failed with a retry delay. Cron skips the instance until retry_not_before is reached. A successful sync clears retry_not_before.

flowchart TD
  subgraph Host[Host backend]
    Instance[Plugin instance]
    Retry{Retry delayed?}
    Skip[Skip for now]
    Sync[Sync instance]
    Error{Needs retry?}
    Store[Store retry_not_before]
    Clear[Clear retry_not_before]
  end

  Instance --> Retry
  Retry -->|yes| Skip
  Retry -->|no| Sync
  Sync --> Error
  Error -->|yes| Store
  Error -->|no| Clear

Sync one instance

Used to prepare one user/plugin instance for sync: actor lookup, runtime selection, auth decryption, OAuth refresh, and capability dispatch.

flowchart TD
  subgraph Host[Host backend]
    Instance[Plugin instance]
    Actor[Find actor]
    Runtime[Select runtime]
    Auth[Decrypt auth]
    Refresh[Refresh OAuth]
    Session[Open WASM session]
    Sync[Sync capabilities]
    Close[Close session]
  end

  subgraph Plugin[WASM plugin]
    Worker([Worker session])
  end

  Instance --> Actor
  Instance --> Runtime
  Instance --> Auth
  Auth --> Refresh
  Actor --> Session
  Runtime --> Session
  Refresh --> Session
  Session --> Worker
  Worker --> Sync
  Sync --> Close

Capabilities

Every plugin capability is declared as a manifest capability. The runtime flow depends on what the capability does: importing trails uses a list/detail pair, while sending a trail asks the plugin for a provider request plan.

Capability: Trail import

Used for one import capability pair such as list_routes.v1 with get_route_detail.v1, or list_activities.v1 with get_activity_detail.v1. This is where provider summaries become imported trails.

flowchart TD
  subgraph Host[Host backend]
    Start[Trail import sync]
    ListCall[Ask plugin for trails]
    Dedupe[Skip known trails]
    Import[Import trail]
  end

  subgraph Plugin[WASM plugin]
    ListExport([List provider trails])
    Summaries[/Trail summaries/]
    DetailExport([Get trail details])
    TrailImport[/Trail import payload/]
  end

  Start --> ListCall
  ListCall --> ListExport
  ListExport --> Summaries
  Summaries --> Dedupe
  Dedupe --> DetailExport
  DetailExport --> TrailImport
  TrailImport --> Import

Capability: Send trail

Used when a user sends an existing wanderer trail to an external provider.

flowchart TD
  subgraph UI[Trail UI]
    Send[Send trail]
  end

  subgraph Host[Host backend]
    Handler[Send trail handler]
    Capability[Load send capability]
    Access[Check access]
    GPX[Read GPX]
    Session[Open WASM session]
    Validate[Validate send plan]
    Auth[Inject auth]
    Execute[Execute request]
    Close[Close session]
  end

  subgraph Plugin[WASM plugin]
    Prepare([Prepare send])
    TrailSendPlan[/TrailSendPlan/]
  end

  subgraph Provider[Provider API]
    ProviderSend[Send trail]
  end

  Send --> Handler
  Handler --> Capability
  Capability --> Access
  Access --> GPX
  GPX --> Session
  Session --> Prepare
  Prepare --> TrailSendPlan
  TrailSendPlan --> Validate
  Validate --> Auth
  Auth --> Execute
  Execute --> ProviderSend
  ProviderSend --> Close