feat: add plugin system (#1034)
* feat: add plugin system * fix db docker build * fix hammerhead readme, add strava subscription news to docs * fixes and sdk improvements * fix: reduce Meilisearch load, debounce federation sync (#1012) * optimize meili trail index * several fixes --------- Co-authored-by: Flomp <Flomp@users.noreply.github.com> * Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032) Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0. - [Release notes](https://github.com/sveltejs/svelte/releases) - [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte) --- updated-dependencies: - dependency-name: svelte dependency-version: 5.56.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Flomp <Flomp@users.noreply.github.com> * Release v0.19.2 (#1035) * chore: release v0.19.2 * add changelog --------- Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com> Co-authored-by: Christian Beutel <> * speed up plugin sync and several small fixes * concepts for security improvements and process stability * improve concept * security concept implemented * remove insecure TLS * worker concept implemented * fixes and cleanup * fixes * docu * mermaid, namings * WASM plugin host improvements, plugin logging * fix db migration * Improve plugin config and category mapping UI * fixes * further fixes * remove manual test sync * fix db migration and strava mapping * type added, UI improvements * fix plugin card toggle clickable area * optimize synch status card layout * plugin type 'trails' instead of 'integration' * session auth validation in UI * fix komoot date and waypoints * improve category mapping * fix send to hammerhead: trail name * plugin setup error handling improved * fix review findings * re-mapping added * rename remote_category --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Flomp <Flomp@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
This commit is contained in:
@@ -83,8 +83,8 @@ export default defineConfig({
|
||||
link: '/use/import-export/'
|
||||
},
|
||||
{
|
||||
label: 'Integrations',
|
||||
link: '/use/integrations/'
|
||||
label: 'Plugins',
|
||||
link: '/use/plugins/'
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -97,6 +97,7 @@ export default defineConfig({
|
||||
{ label: 'Quickstart', link: '/run/installation/quick' },
|
||||
{ label: 'Manual Docker Setup', link: '/run/installation/docker' },
|
||||
{ label: 'Install from Source', link: '/run/installation/from-source' },
|
||||
{ label: 'Plugin installation', link: '/run/installation/plugins' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -137,6 +138,10 @@ export default defineConfig({
|
||||
label: 'Federation',
|
||||
link: '/develop/federation/'
|
||||
},
|
||||
{
|
||||
label: 'Plugin System',
|
||||
link: '/develop/plugin-system/'
|
||||
},
|
||||
]
|
||||
},
|
||||
...openAPISidebarGroups,
|
||||
|
||||
923
docs/src/content/docs/develop/plugin-system.md
Normal file
923
docs/src/content/docs/develop/plugin-system.md
Normal file
@@ -0,0 +1,923 @@
|
||||
---
|
||||
title: Plugin System
|
||||
description: Build, install, and run WASM provider plugins in wanderer
|
||||
---
|
||||
|
||||
Plugins let wanderer connect to external providers such as Strava, komoot, and
|
||||
Hammerhead without adding provider-specific API code to the core application.
|
||||
|
||||
A plugin is a local directory with a `plugin.json` manifest and a WASM
|
||||
entrypoint:
|
||||
|
||||
```text
|
||||
data/plugins/
|
||||
strava/
|
||||
plugin.json
|
||||
plugin.wasm
|
||||
icon.svg
|
||||
```
|
||||
|
||||
wanderer discovers plugins from direct child directories of `data/plugins`.
|
||||
Plugin configuration, credentials, sync state, and status are stored per user in
|
||||
`plugin_instances`.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Use an existing first-party plugin as a starting point:
|
||||
|
||||
- [Hammerhead plugin source](https://github.com/open-wanderer/wanderer/tree/main/plugins/hammerhead)
|
||||
- [komoot plugin source](https://github.com/open-wanderer/wanderer/tree/main/plugins/komoot)
|
||||
- [Strava plugin source](https://github.com/open-wanderer/wanderer/tree/main/plugins/strava)
|
||||
|
||||
For local development:
|
||||
|
||||
```sh
|
||||
make plugins-build
|
||||
make plugins-install-local
|
||||
```
|
||||
|
||||
Start wanderer and open the plugin settings page. The plugin should appear once
|
||||
its bundle exists at:
|
||||
|
||||
```text
|
||||
data/plugins/<plugin-id>/plugin.json
|
||||
data/plugins/<plugin-id>/plugin.wasm
|
||||
```
|
||||
|
||||
## 1st-party plugins
|
||||
|
||||
First-party plugin source lives in the repository under `plugins/`:
|
||||
|
||||
```text
|
||||
plugins/
|
||||
hammerhead/
|
||||
komoot/
|
||||
strava/
|
||||
sdk/
|
||||
```
|
||||
|
||||
Build all bundled plugins:
|
||||
|
||||
```sh
|
||||
make plugins-build
|
||||
```
|
||||
|
||||
Build and install them into the local runtime directory:
|
||||
|
||||
```sh
|
||||
make plugins-install-local
|
||||
```
|
||||
|
||||
Package release archives:
|
||||
|
||||
```sh
|
||||
make plugins-package
|
||||
```
|
||||
|
||||
Release archives are published as separate GitHub release assets. The database
|
||||
Docker image does not contain provider plugins.
|
||||
|
||||
## Plugin layout
|
||||
|
||||
A provider plugin should use this layout:
|
||||
|
||||
```text
|
||||
plugins/<provider>/
|
||||
go.mod
|
||||
plugin.json
|
||||
main.go
|
||||
assets/icon.svg
|
||||
Makefile
|
||||
```
|
||||
|
||||
Generated runtime files are written to `dist/<plugin-id>/` and are ignored by
|
||||
git:
|
||||
|
||||
```text
|
||||
plugins/strava/dist/strava/
|
||||
plugin.json
|
||||
plugin.wasm
|
||||
icon.svg
|
||||
```
|
||||
|
||||
The generated `dist/<plugin-id>` directory is the directory users install below
|
||||
`data/plugins`.
|
||||
|
||||
Icons are referenced from `plugin.json` metadata and copied from `assets/` into
|
||||
the dist directory by the plugin `Makefile`:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"icons": {
|
||||
"light": "icon.svg",
|
||||
"dark": "icon_dark.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`dark` is optional.
|
||||
|
||||
## Go SDK
|
||||
|
||||
Go/TinyGo plugins should import the plugin SDK:
|
||||
|
||||
```go
|
||||
import "github.com/open-wanderer/wanderer/plugins/sdk"
|
||||
```
|
||||
|
||||
The SDK contains plugin-side protocol types and host-function helpers. It does
|
||||
not depend on wanderer core or PocketBase.
|
||||
|
||||
Most plugins use:
|
||||
|
||||
- `sdk.HostRequest` for provider API calls through `wanderer.http_request`
|
||||
- `sdk.Get` and `sdk.PostJSON` convenience helpers
|
||||
- `sdk.HostRequestSpec`, `sdk.ResponseExpect`, and multipart body constants
|
||||
- auth/header constants such as `sdk.AuthHeaderAuthorization`
|
||||
|
||||
## Manifest
|
||||
|
||||
Each plugin must define a static `plugin.json` manifest. The manifest is the
|
||||
security and capability contract used by the host.
|
||||
|
||||
The repository includes a JSON Schema at
|
||||
`plugins/schema/plugin.schema.json`. Add a `$schema` field in source manifests
|
||||
to get editor completion and inline validation:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "../schema/plugin.schema.json"
|
||||
}
|
||||
```
|
||||
|
||||
Minimal shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifestVersion": "1.0",
|
||||
"id": "example",
|
||||
"type": "trails",
|
||||
"name": "Example",
|
||||
"version": "0.1.0",
|
||||
"runtime": {
|
||||
"type": "wasm",
|
||||
"entrypoint": "plugin.wasm"
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"name": "list_routes",
|
||||
"version": "v1",
|
||||
"export": "list_routes_v1"
|
||||
},
|
||||
{
|
||||
"name": "get_route_detail",
|
||||
"version": "v1",
|
||||
"export": "get_route_detail_v1"
|
||||
}
|
||||
],
|
||||
"permissions": {
|
||||
"network": {
|
||||
"connectors": [
|
||||
{
|
||||
"name": "api",
|
||||
"type": "public_api",
|
||||
"fixedBaseURL": "https://api.example.com",
|
||||
"allowedPathPrefixes": ["/v1"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"downloads": {
|
||||
"maxBytes": 1048576,
|
||||
"contentTypes": ["application/json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Important rules:
|
||||
|
||||
- `type` is the functional plugin category. Currently only `trails` is supported.
|
||||
- `runtime.entrypoint` must be relative to the plugin directory.
|
||||
- `id` must match the installed directory name by convention.
|
||||
- `capabilities[].export` names the WASM export the runtime calls.
|
||||
- `permissions.network.connectors` declares every provider target the plugin may
|
||||
request through the host.
|
||||
- per-request limits may narrow manifest limits, but never expand them.
|
||||
- `configSchema[].required` marks plugin-owned settings that the settings UI
|
||||
must collect before saving.
|
||||
|
||||
### Network connectors
|
||||
|
||||
Provider HTTP is connector-based. Plugins do not send absolute provider URLs to
|
||||
the host; they name a connector and a relative path. The host resolves that
|
||||
connector to a concrete base URL, validates the path scope, injects auth, and
|
||||
executes the request.
|
||||
|
||||
Connector types:
|
||||
|
||||
| Type | Purpose |
|
||||
| --- | --- |
|
||||
| `public_api` | Fixed public provider API declared in the manifest. Use this for SaaS APIs such as Strava, komoot, or Hammerhead. |
|
||||
| `configured` | Provider target configured by the host under `config.host.connectors`. Use this for self-hosted services. |
|
||||
|
||||
`public_api` connectors must declare `fixedBaseURL`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "api",
|
||||
"type": "public_api",
|
||||
"fixedBaseURL": "https://api.example.com",
|
||||
"allowedPathPrefixes": ["/v1"],
|
||||
"auth": ["oauth_access_token"]
|
||||
}
|
||||
```
|
||||
|
||||
`configured` connectors must declare `configKey`; the host supplies the concrete
|
||||
base URL and trust settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "media",
|
||||
"type": "configured",
|
||||
"configKey": "immich",
|
||||
"allowedPathPrefixes": ["/api"],
|
||||
"auth": ["api_key"],
|
||||
"supportsMediaAuth": true,
|
||||
"supportsStorageRedirects": true,
|
||||
"supportsCustomTLS": true
|
||||
}
|
||||
```
|
||||
|
||||
Connector fields:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `name` | Connector identifier used by `HostRequestSpec.target.connector` and `MediaRef.connector`. |
|
||||
| `type` | `public_api` or `configured`. |
|
||||
| `fixedBaseURL` | Fixed URL for public APIs. Must not include credentials, query, or fragment. |
|
||||
| `configKey` | Host config key for configured connectors. |
|
||||
| `allowedPathPrefixes` | Relative provider paths the plugin may request. Defaults to `/` when empty. |
|
||||
| `auth` | Auth contexts allowed for this connector. |
|
||||
| `supportsMediaAuth` | Allows connector media downloads to reference an auth context. |
|
||||
| `supportsStorageRedirects` | Allows connector media downloads to redirect to configured storage origins. |
|
||||
| `supportsCustomTLS` | Allows the host to attach a custom CA bundle to this connector. |
|
||||
|
||||
The host validates scheme, host, effective port, base path, path prefixes,
|
||||
redirect targets, TLS policy, and IP policy. `allowPrivate`, custom CA bundles,
|
||||
and storage origins are host-owned settings; plugin output can never enable
|
||||
private-network access.
|
||||
|
||||
## Capabilities
|
||||
|
||||
Implemented sync/send capabilities:
|
||||
|
||||
| Capability | Export Example | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `list_routes.v1` | `list_routes_v1` | List planned route IDs |
|
||||
| `get_route_detail.v1` | `get_route_detail_v1` | Return one planned route import |
|
||||
| `list_activities.v1` | `list_activities_v1` | List completed activity IDs |
|
||||
| `get_activity_detail.v1` | `get_activity_detail_v1` | Return one completed activity import |
|
||||
| `prepare_trail_send.v1` | `prepare_trail_send_v1` | Prepare sending a trail |
|
||||
|
||||
Import sync is a two-step protocol. A plugin that declares `list_routes.v1`
|
||||
must also declare `get_route_detail.v1`; a plugin that declares
|
||||
`list_activities.v1` must also declare `get_activity_detail.v1`. If the matching
|
||||
detail capability is missing, the host skips that list capability and logs a
|
||||
warning. This is a breaking change from older one-step sync plugins whose
|
||||
`list_*` exports returned full trail imports.
|
||||
|
||||
Session-based plugins may also export an auth refresh function declared by the
|
||||
manifest, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"contexts": {
|
||||
"provider_session": {
|
||||
"type": "session",
|
||||
"fields": ["email", "password"],
|
||||
"secretFields": ["password"],
|
||||
"refresh": {
|
||||
"mode": "plugin",
|
||||
"function": "refresh_session_v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sync input
|
||||
|
||||
`list_routes_v1` and `list_activities_v1` receive JSON input:
|
||||
|
||||
```json
|
||||
{
|
||||
"instance": {
|
||||
"id": "abc123",
|
||||
"pluginId": "strava"
|
||||
},
|
||||
"auth": {},
|
||||
"state": {},
|
||||
"options": {
|
||||
"after": "2026-01-01"
|
||||
},
|
||||
"limits": {
|
||||
"maxItems": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`auth` contains only values the host is allowed to pass to the plugin. For
|
||||
OAuth plugins, refresh tokens and client secrets are not included in normal sync
|
||||
capability input. Depending on the auth model, `auth` may contain values such
|
||||
as:
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": "short-lived-token"
|
||||
}
|
||||
```
|
||||
|
||||
or, for session-based providers:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "encrypted-at-rest-but-decrypted-for-plugin-login"
|
||||
}
|
||||
```
|
||||
|
||||
## List output
|
||||
|
||||
List capabilities return lightweight summaries plus capability-local state. The
|
||||
host uses `source.provider` and `source.externalId` for deduplication and calls
|
||||
the matching detail capability only for new items.
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"source": {
|
||||
"provider": "strava",
|
||||
"externalId": "123",
|
||||
"url": "https://provider.example/routes/123"
|
||||
},
|
||||
"kind": "planned"
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"page": 2
|
||||
},
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
State returned by a plugin is first fed back into the next batch of the same
|
||||
sync run. Only persistent provider cursors belong in `plugin_instances.state`.
|
||||
Transient batch cursors such as `page` are not stored in the database.
|
||||
|
||||
## Detail input
|
||||
|
||||
`get_route_detail_v1` and `get_activity_detail_v1` receive the summary selected
|
||||
by the host:
|
||||
|
||||
```json
|
||||
{
|
||||
"instance": {
|
||||
"id": "abc123",
|
||||
"pluginId": "strava"
|
||||
},
|
||||
"auth": {},
|
||||
"options": {
|
||||
"after": "2026-01-01"
|
||||
},
|
||||
"summary": {
|
||||
"source": {
|
||||
"provider": "strava",
|
||||
"externalId": "123"
|
||||
},
|
||||
"kind": "planned"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Detail output
|
||||
|
||||
Detail capabilities return the full trail import:
|
||||
|
||||
```json
|
||||
{
|
||||
"item": {
|
||||
"source": {
|
||||
"provider": "strava",
|
||||
"externalId": "123",
|
||||
"url": "https://provider.example/routes/123"
|
||||
},
|
||||
"kind": "planned",
|
||||
"name": "Morning Ride",
|
||||
"track": {
|
||||
"format": "gpx",
|
||||
"contentBase64": "..."
|
||||
},
|
||||
"waypoints": [
|
||||
{
|
||||
"name": "Viewpoint",
|
||||
"lat": 47.3769,
|
||||
"lon": 8.5417,
|
||||
"photos": [
|
||||
{
|
||||
"filename": "viewpoint.jpg",
|
||||
"contentType": "image/jpeg",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": "https://provider.example/photo.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"distance": 12345.6,
|
||||
"elevationGain": 320.5,
|
||||
"elevationLoss": 318.1,
|
||||
"duration": 4567,
|
||||
"providerCategory": "Ride"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The host imports the trails, writes PocketBase records, applies visibility
|
||||
rules, deduplicates by provider/external ID, and stores the returned state.
|
||||
Trail photos are attached to the imported trail. Waypoint photos are attached to
|
||||
the corresponding waypoint records. Waypoint `distance_from_start` is derived
|
||||
by the host from the nearest position on the imported GPX track.
|
||||
|
||||
Media sources have two trust models:
|
||||
|
||||
| Source type | Meaning |
|
||||
| --- | --- |
|
||||
| `url` | Public external media URL. The host fetches it with public-only SSRF protections and bounded size limits. |
|
||||
| `connector` | Provider-owned media fetched through a declared connector, optional host-injected auth, connector TLS/IP policy, and connector-scoped redirects. |
|
||||
|
||||
Public media example:
|
||||
|
||||
```json
|
||||
{
|
||||
"filename": "cover.jpg",
|
||||
"contentType": "image/jpeg",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": "https://cdn.example.com/photos/cover.jpg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Connector media example:
|
||||
|
||||
```json
|
||||
{
|
||||
"filename": "original.jpg",
|
||||
"contentType": "image/jpeg",
|
||||
"source": {
|
||||
"type": "connector",
|
||||
"mediaRef": {
|
||||
"connector": "media",
|
||||
"auth": "api_key",
|
||||
"path": "/api/assets/123/original",
|
||||
"query": [
|
||||
{ "name": "size", "value": "preview" }
|
||||
],
|
||||
"assetId": "123"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`mediaRef.path` is required for connector downloads. `assetId` is metadata only
|
||||
for now; the host does not resolve `assetId` into a URL.
|
||||
|
||||
Plugins should return GPX as the canonical track. If the provider exposes
|
||||
authoritative summary metrics, the plugin may additionally return them in
|
||||
`metadata`:
|
||||
|
||||
| Metadata key | Unit | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `distance` | meters | Provider-reported trail distance. |
|
||||
| `elevationGain` | meters | Provider-reported positive elevation gain. |
|
||||
| `elevationLoss` | meters | Provider-reported negative elevation loss. |
|
||||
| `duration` | seconds | Provider-reported elapsed duration. |
|
||||
| `providerStart` | object | Provider-reported intended start coordinate, for example `{ "lat": 47.123, "lon": 8.456 }`. |
|
||||
| `providerCategory` | string | Raw provider activity/category value used by host category mapping. |
|
||||
|
||||
The host uses positive provider metrics when present and falls back to GPX
|
||||
derived metrics otherwise. Start location comes from the GPX unless
|
||||
`providerStart` is present and close enough to the imported GPX track to be
|
||||
plausible. Plugins should not map `providerCategory` to local category IDs; the
|
||||
host owns that mapping.
|
||||
|
||||
## Host config
|
||||
|
||||
Plugin manifests may suggest defaults for host-owned settings with
|
||||
`hostConfig`. These values are stored in `installed_plugins.config.host` and can
|
||||
be overridden per plugin instance with `plugin_instances.config.host`. Host
|
||||
config is never passed to plugin exports.
|
||||
|
||||
Supported host fields:
|
||||
|
||||
| Field | Type | Used by | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `planned` | boolean | `list_routes.v1` | Enables planned route sync for the instance. |
|
||||
| `completed` | boolean | `list_activities.v1` | Enables completed activity sync for the instance. |
|
||||
| `privacy` | string | Trail import | `original` keeps provider visibility; `settings` uses the local user trail privacy setting. |
|
||||
| `merge.enabled` | boolean | Trail import | Runs auto-merge after creating imported trails. |
|
||||
| `createSummitLogForCompleted` | boolean | Trail import | Creates summit logs for completed imported trails. Defaults to `true`. |
|
||||
| `categoryMapping` | object | Trail import | Maps plugin-provided `metadata.providerCategory` values to local category IDs or category names. |
|
||||
| `connectors` | object | Host request/media policy | Concrete settings for configured connectors. |
|
||||
|
||||
The settings UI lets users edit `categoryMapping` per plugin instance for trail
|
||||
import plugins. Unknown or empty provider categories still fall back to the
|
||||
host's activity-type mapping.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostConfig": {
|
||||
"categoryMapping": {
|
||||
"Ride": "Biking",
|
||||
"Hike": "Hiking"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"providerCategories": {
|
||||
"Ride": {
|
||||
"labels": {
|
||||
"de": "Radfahren",
|
||||
"en": "Ride"
|
||||
}
|
||||
},
|
||||
"Hike": {
|
||||
"labels": {
|
||||
"de": "Wandern",
|
||||
"en": "Hike"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`metadata.providerCategories` is display-only metadata for provider-owned
|
||||
category values. The `categoryMapping` keys still use the raw values emitted as
|
||||
`metadata.providerCategory`.
|
||||
|
||||
Configured connector host config shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostConfig": {
|
||||
"connectors": {
|
||||
"immich": {
|
||||
"baseURL": "https://photos.example.com",
|
||||
"basePath": "/immich",
|
||||
"allowPrivate": false,
|
||||
"tls": {
|
||||
"mode": "system"
|
||||
},
|
||||
"storageOrigins": {
|
||||
"object-storage": {
|
||||
"baseURL": "https://storage.example.com",
|
||||
"basePath": "/assets",
|
||||
"allowPrivate": false,
|
||||
"tls": {
|
||||
"mode": "system"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`tls.mode` supports `system` and `customCA`. Custom CA bundles are trusted only
|
||||
when the manifest connector declares `supportsCustomTLS`; certificate
|
||||
verification is not disabled.
|
||||
|
||||
The host defines the semantics of these fields. Plugins only provide defaults
|
||||
or hints; custom plugin settings belong in `configSchema` and are passed to the
|
||||
plugin under `options`.
|
||||
|
||||
Plugin errors should use the structured error format:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "rate_limited",
|
||||
"message": "Provider rate limit exceeded",
|
||||
"retryAfterSeconds": 3600
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported status-relevant error codes include:
|
||||
|
||||
```text
|
||||
auth_failed
|
||||
invalid_grant
|
||||
unauthorized
|
||||
rate_limited
|
||||
provider_unavailable
|
||||
temporary_unavailable
|
||||
```
|
||||
|
||||
## Host requests
|
||||
|
||||
Plugins cannot perform arbitrary provider I/O. They ask the host to execute
|
||||
provider requests through the WASM host function `wanderer.http_request`.
|
||||
Absolute provider URLs are not part of the request ABI.
|
||||
|
||||
The request shape is `HostRequestSpec`:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "GET",
|
||||
"target": {
|
||||
"type": "connector",
|
||||
"connector": "api",
|
||||
"path": "/routes",
|
||||
"query": [
|
||||
{ "name": "page", "value": "1" }
|
||||
]
|
||||
},
|
||||
"auth": "oauth_access_token",
|
||||
"headers": {
|
||||
"accept": "application/json"
|
||||
},
|
||||
"expect": {
|
||||
"contentTypes": ["application/json"],
|
||||
"maxBytes": 1048576
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The host validates:
|
||||
|
||||
- connector identity, scheme, host, effective port, base path, and path scope
|
||||
- auth context reference and connector-specific auth allowance
|
||||
- manifest network permissions
|
||||
- response content type
|
||||
- response size
|
||||
- redirect target scope
|
||||
|
||||
The shared Go SDK wraps this host function:
|
||||
|
||||
```go
|
||||
response, body, err := sdk.HostRequest(sdk.HostRequestSpec{
|
||||
Method: "GET",
|
||||
Target: sdk.RequestTarget{
|
||||
Type: "connector",
|
||||
Connector: "api",
|
||||
Path: "/routes",
|
||||
Query: []sdk.QueryParam{{Name: "page", Value: "1"}},
|
||||
},
|
||||
Expect: sdk.ResponseExpect{
|
||||
ContentTypes: []string{"application/json"},
|
||||
MaxBytes: 1048576,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Auth referenced by `HostRequestSpec.auth` is injected by the host. OAuth,
|
||||
bearer, and API-key contexts are supported for plugin-initiated host requests.
|
||||
Session auth requires handler-managed injection; if a plugin calls
|
||||
`wanderer.http_request` with a session auth context, the host rejects the
|
||||
request instead of silently sending it unauthenticated.
|
||||
|
||||
## Sending trails
|
||||
|
||||
`prepare_trail_send_v1` receives the trail GPX from wanderer and returns a
|
||||
send plan. The plugin prepares the provider-specific request; the host
|
||||
executes it.
|
||||
|
||||
Input:
|
||||
|
||||
```json
|
||||
{
|
||||
"instance": {
|
||||
"id": "abc123",
|
||||
"pluginId": "hammerhead"
|
||||
},
|
||||
"auth": {},
|
||||
"config": {},
|
||||
"name": "Lunch Loop",
|
||||
"trail": {
|
||||
"format": "gpx",
|
||||
"contentBase64": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`config` contains the saved plugin instance configuration, for example sync
|
||||
modes, an `after` date, or provider-specific options. `auth` follows the same
|
||||
rules as sync input.
|
||||
|
||||
Output:
|
||||
|
||||
```json
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"target": {
|
||||
"type": "connector",
|
||||
"connector": "api",
|
||||
"path": "/routes"
|
||||
},
|
||||
"auth": "provider_session",
|
||||
"body": {
|
||||
"type": "multipart",
|
||||
"parts": [
|
||||
{
|
||||
"name": "file",
|
||||
"source": "trail"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": {
|
||||
"contentTypes": ["application/json"],
|
||||
"maxBytes": 1048576
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported multipart trail sources:
|
||||
|
||||
```text
|
||||
trail
|
||||
trail.gpx
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
Auth contexts are declared in the manifest and referenced by name from
|
||||
`HostRequestSpec.auth`.
|
||||
|
||||
### OAuth2
|
||||
|
||||
OAuth is declarative. The host runs authorization, token exchange, token
|
||||
storage, and refresh:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"contexts": {
|
||||
"oauth_access_token": {
|
||||
"type": "oauth2",
|
||||
"fields": ["clientId", "clientSecret"],
|
||||
"secretFields": ["clientSecret", "accessToken", "refreshToken"],
|
||||
"authorizationUrl": "https://provider.example/oauth/authorize",
|
||||
"tokenUrl": "https://provider.example/oauth/token",
|
||||
"scopes": ["activity:read_all"],
|
||||
"scopeSeparator": ",",
|
||||
"tokenRequestFormat": "json",
|
||||
"tokenAuth": "client_secret_post",
|
||||
"refresh": {
|
||||
"mode": "host",
|
||||
"grantType": "refresh_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The plugin may receive the short-lived access token in normal capability input.
|
||||
It does not receive refresh tokens or client secrets during normal sync.
|
||||
OAuth token endpoints must be covered by a fixed `public_api` connector in the
|
||||
manifest. Token exchange does not use user-configured connector origins.
|
||||
|
||||
### Session
|
||||
|
||||
Session auth is for providers that require plugin-mediated login:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"contexts": {
|
||||
"provider_session": {
|
||||
"type": "session",
|
||||
"fields": ["email", "password"],
|
||||
"secretFields": ["password"],
|
||||
"refresh": {
|
||||
"mode": "plugin",
|
||||
"function": "refresh_session_v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The host passes only the declared secret fields to the refresh export. The
|
||||
returned session token is stored encrypted and injected by the host into future
|
||||
handler-managed host-executed requests that reference the auth context, such as
|
||||
`prepare_trail_send.v1` send plans. Plugin-initiated `wanderer.http_request`
|
||||
calls cannot refresh session auth themselves.
|
||||
|
||||
### API key and bearer
|
||||
|
||||
API key and bearer contexts use a configured secret field:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"contexts": {
|
||||
"api_key": {
|
||||
"type": "api_key",
|
||||
"placement": "header",
|
||||
"name": "x-api-key",
|
||||
"secretField": "apiKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Runtime isolation
|
||||
|
||||
WASM plugins run in a separate worker process for each sync or trail-upload job.
|
||||
All exports within that job share the same worker session and are called
|
||||
sequentially. If a plugin calls `wanderer.http_request`, the worker forwards the
|
||||
request bytes back to the backend; the backend remains the only process that
|
||||
holds connector policy, decrypted host auth, custom CA bundles, and HTTP
|
||||
execution logic.
|
||||
|
||||
The worker boundary protects the backend from plugin crashes and hangs and
|
||||
enforces request/response frame limits and timeouts. It is not an OS-level
|
||||
sandbox for outbound network access; plugin-controlled provider traffic must
|
||||
still go through the host request API.
|
||||
|
||||
## Plugin state
|
||||
|
||||
User plugin configuration is stored in `plugin_instances`:
|
||||
|
||||
```text
|
||||
plugin_instances
|
||||
user
|
||||
plugin_id
|
||||
enabled
|
||||
auth
|
||||
config
|
||||
state
|
||||
status
|
||||
last_error
|
||||
last_sync_at
|
||||
retry_not_before
|
||||
```
|
||||
|
||||
`auth` is encrypted by PocketBase hooks. `config.plugin` stores settings passed
|
||||
to the plugin, such as an `after` date. `config.host` stores host-owned settings
|
||||
such as enabled capabilities, privacy handling, merge settings, and category
|
||||
mapping. `state` stores per-capability provider cursors. It should only contain
|
||||
values that remain valid across separate sync runs, such as provider sync tokens
|
||||
or delta cursors. Batch-local cursors such as `page` are discarded before the
|
||||
instance is saved.
|
||||
|
||||
The host also caches discovered plugin manifests in `installed_plugins`.
|
||||
Installed plugins and user plugin instances are intentionally separate:
|
||||
`installed_plugins.config` stores admin defaults, while
|
||||
`plugin_instances.config` stores per-instance overrides. A user configuration
|
||||
can exist even if the plugin bundle is not currently installed.
|
||||
|
||||
## Release and installation
|
||||
|
||||
The release workflow builds plugin archives:
|
||||
|
||||
```text
|
||||
wanderer-plugin-hammerhead.tar.gz
|
||||
wanderer-plugin-komoot.tar.gz
|
||||
wanderer-plugin-strava.tar.gz
|
||||
SHA256SUMS
|
||||
```
|
||||
|
||||
Users install a plugin by extracting the archive below `data/plugins`:
|
||||
|
||||
```text
|
||||
data/plugins/hammerhead/plugin.json
|
||||
data/plugins/hammerhead/plugin.wasm
|
||||
```
|
||||
|
||||
Docker deployments mount the runtime directory into the DB container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
volumes:
|
||||
- ./data/plugins:/data/plugins
|
||||
```
|
||||
@@ -21,18 +21,22 @@ Since we use an unmodified installation of meilisearch you can use all variables
|
||||
| MEILI_NO_ANALYTICS | Disable meilisearch telemetry | true |
|
||||
|
||||
## Pocketbase
|
||||
| Environment Variable | Description | Default |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| ORIGIN | Public IP or hostname (including the port) of your <span class="-tracking-[0.075em]">wanderer</span> frontend (must be the same as in the frontend config) | http://localhost:3000 |
|
||||
| POCKETBASE_ENCRYPTION_KEY | Valid 32 character AES key. Used to encrypt secrets | |
|
||||
| POCKETBASE_CRON_SYNC_SCHEDULE | Valid cron expression. Sets how often trails are synced from 3rd party integrations | 0 2 * * * |
|
||||
| POCKETBASE_SMTP_ENABLED | Enables or disables SMTP functionality. Accepted values are true or false | false |
|
||||
| Environment Variable | Description | Default |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| ORIGIN | Public IP or hostname (including the port) of your <span class="-tracking-[0.075em]">wanderer</span> frontend (must be the same as in the frontend config) | <http://localhost:3000> |
|
||||
| POCKETBASE_ENCRYPTION_KEY | Valid 32 character AES key. Used to encrypt secrets | |
|
||||
| POCKETBASE_CRON_SYNC_SCHEDULE | Valid cron expression. Sets how often installed plugins are synced | 0 2 ** * |
|
||||
| POCKETBASE_SMTP_ENABLED | Enables or disables SMTP functionality. Accepted values are true or false | false |
|
||||
| POCKETBASE_SMTP_SENDER_ADDRESS | The email address used as the "From" address in outgoing emails | |
|
||||
| POCKETBASE_SMTP_SENDER_NAME | The display name shown as the sender in outgoing emails | |
|
||||
| POCKETBASE_SMTP_HOST | The hostname or IP address of the SMTP server | |
|
||||
| POCKETBASE_SMTP_PORT | The port number used to connect to the SMTP server | |
|
||||
| POCKETBASE_SMTP_USERNAME | The username used to authenticate with the SMTP server | |
|
||||
| POCKETBASE_SMTP_PASSWORD | The password used to authenticate with the SMTP server | |
|
||||
| POCKETBASE_SMTP_SENDER_NAME | The display name shown as the sender in outgoing emails | |
|
||||
| POCKETBASE_SMTP_HOST | The hostname or IP address of the SMTP server | |
|
||||
| POCKETBASE_SMTP_PORT | The port number used to connect to the SMTP server | |
|
||||
| POCKETBASE_SMTP_USERNAME | The username used to authenticate with the SMTP server | |
|
||||
| POCKETBASE_SMTP_PASSWORD | The password used to authenticate with the SMTP server | |
|
||||
|
||||
Plugins are not configured through an environment variable. See
|
||||
[Plugin installation](/run/installation/plugins) for installing runtime plugin
|
||||
bundles and configuring self-hosted connector trust settings.
|
||||
|
||||
## Frontend
|
||||
|
||||
@@ -78,3 +82,6 @@ services:
|
||||
volumes:
|
||||
- ./certs/ca.pem:/etc/ssl/private-ca/ca.pem:ro
|
||||
```
|
||||
|
||||
Provider plugin connector CAs are configured per connector when a plugin
|
||||
supports custom TLS. They are not read from `NODE_EXTRA_CA_CERTS`.
|
||||
|
||||
59
docs/src/content/docs/run/installation/plugins.md
Normal file
59
docs/src/content/docs/run/installation/plugins.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Plugin installation
|
||||
description: How to install and operate provider plugins
|
||||
---
|
||||
|
||||
Provider integrations are installed as local WASM plugin bundles. A runtime
|
||||
plugin bundle is a directory with at least:
|
||||
|
||||
```text
|
||||
plugin.json
|
||||
plugin.wasm
|
||||
```
|
||||
|
||||
Install each extracted bundle as a direct child directory of `data/plugins`:
|
||||
|
||||
```text
|
||||
data/plugins/strava/plugin.json
|
||||
data/plugins/strava/plugin.wasm
|
||||
```
|
||||
|
||||
wanderer discovers plugins from `data/plugins/<plugin-id>/plugin.json`. After
|
||||
discovery, the plugin appears in the plugin settings page.
|
||||
|
||||
## Installing release bundles
|
||||
|
||||
Official Docker images do not include provider plugins. Download plugin bundle
|
||||
archives from the GitHub release assets, extract them, and copy the extracted
|
||||
plugin directory into the mounted `./data/plugins` directory.
|
||||
|
||||
There is no built-in plugin store. Community plugins can be installed the same
|
||||
way, but only install plugin bundles from sources you trust.
|
||||
|
||||
## Source checkout
|
||||
|
||||
When running from a source checkout, first-party plugin source lives under the
|
||||
repository's `plugins/` directory. That source directory is not the runtime
|
||||
install location.
|
||||
|
||||
Build and install the bundled plugins into `data/plugins` with:
|
||||
|
||||
```sh
|
||||
make plugins-install-local
|
||||
```
|
||||
|
||||
Use this after a fresh checkout or after changing first-party plugin code.
|
||||
|
||||
## Runtime and network model
|
||||
|
||||
Plugins run as local WASM modules in a separate worker process. Provider API and
|
||||
media requests are still executed by the backend through the plugin manifest's
|
||||
network policy; plugins do not get unrestricted access to your server network.
|
||||
|
||||
Self-hosted provider plugins may expose connector settings such as a base URL,
|
||||
private-network access, storage redirect origins, or a custom CA bundle. Treat
|
||||
those settings as administrator trust decisions: only enable private-network
|
||||
access or custom CAs for plugin bundles and endpoints you trust.
|
||||
|
||||
Provider plugin connector CAs are configured per connector when a plugin
|
||||
supports custom TLS. They are not read from `NODE_EXTRA_CA_CERTS`.
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
title: Integrations
|
||||
description: How to set up third-party integrations with wanderer.
|
||||
---
|
||||
|
||||
You can automatically sync trails to <span class="-tracking-[0.075em]">wanderer</span> at regular intervals using the third-party integration feature. Currently, we support three providers: **Strava**, **komoot** and **hammerhead**.
|
||||
|
||||
It is important to note that synchronization only works from the provider to <span class="-tracking-[0.075em]">wanderer</span> and not the other way around. Additionally, if a trail has already been synced to <span class="-tracking-[0.075em]">wanderer</span>, subsequent changes made in the provider will not be transferred unless the trail is deleted in <span class="-tracking-[0.075em]">wanderer</span>. Hammerhead also supports manual uploads from a trail's action menu, which is separate from the nightly sync.
|
||||
|
||||
## Strava Integration
|
||||
|
||||
### Creating an App in Strava
|
||||
|
||||
Before integrating Strava with <span class="-tracking-[0.075em]">wanderer</span>, you need to create an API application in Strava. Visit [Strava's API settings](https://www.strava.com/settings/api) and follow the steps to create a new API application. Your setup should resemble the following:
|
||||
|
||||

|
||||
|
||||
### Setting Up the Integration
|
||||
|
||||
1. Copy the **Client ID** and **Client Secret**.
|
||||
2. Go to the integrations page in <span class="-tracking-[0.075em]">wanderer</span>'s settings.
|
||||
3. Click the settings button for the Strava integration.
|
||||
4. Enter your **Client ID** and **Client Secret**.
|
||||
5. Choose whether you want to sync routes, activities, or both.
|
||||
|
||||

|
||||
|
||||
6. Save the settings and toggle the integration on.
|
||||
7. You will be redirected to Strava's authorization page. Keep all checkboxes selected and click **Authorize**.
|
||||
8. You will then be redirected back to <span class="-tracking-[0.075em]">wanderer</span>. The Strava integration is now active.
|
||||
|
||||
## komoot Integration
|
||||
|
||||
The komoot integration requires only your komoot username and password:
|
||||
|
||||
1. Open the komoot settings from the integrations menu.
|
||||
2. Enter your komoot credentials.
|
||||
3. Save the settings.
|
||||
4. Toggle the integration on. It will become active immediately.
|
||||
|
||||
Your planned and completed trails will now sync with <span class="-tracking-[0.075em]">wanderer</span>.
|
||||
|
||||
## Hammerhead Integration
|
||||
|
||||
The Hammerhead integration requires your Hammerhead account details:
|
||||
|
||||
1. Open the Hammerhead settings from the integrations menu.
|
||||
2. Enter your Hammerhead email and password.
|
||||
3. Choose whether you want to sync planned tours, completed tours, or both.
|
||||
4. (Optional) Set an "ignore trails before" date to avoid syncing duplicates if your Hammerhead account is already connected to other services.
|
||||
5. Save the settings and toggle the integration on. It will become active immediately after a successful login.
|
||||
|
||||
## Sync Interval
|
||||
|
||||
By default, trails are synced every night at **02:00 AM**. You can modify this schedule using the `POCKETBASE_CRON_SYNC_SCHEDULE` [environment variable](/run/environment-configuration#pocketbase).
|
||||
|
||||
:::note
|
||||
Please set a reasonable sync interval. Both Strava and komoot impose usage limits on their APIs. Exceeding these limits may result in rejected requests or account suspension.
|
||||
:::
|
||||
@@ -28,7 +28,7 @@ Before the merge is executed, <span class="-tracking-[0.075em]">wanderer</span>
|
||||
The target suggestion currently considers:
|
||||
|
||||
- existing summit logs
|
||||
- external references from integrations
|
||||
- external references from plugins
|
||||
- content richness such as comments, photos, waypoints and descriptions
|
||||
- how centrally the trail geometry fits within the candidate set
|
||||
- trail age as a deterministic fallback
|
||||
@@ -50,11 +50,11 @@ The maintenance page groups potentially repeated or duplicate trails so that you
|
||||
|
||||
This page is especially useful after large imports or when you want to consolidate older data.
|
||||
|
||||
## Integrations
|
||||
## Plugins
|
||||
|
||||
Integrations can optionally auto-merge imported trails, but only when the backend finds exactly one clear target candidate. This keeps imports conservative and avoids accidentally merging different routes.
|
||||
Plugins can optionally auto-merge imported trails, but only when the backend finds exactly one clear target candidate. This keeps imports conservative and avoids accidentally merging different routes.
|
||||
|
||||
External references from integrations are preserved during merges, so future imports can still recognize already-linked trails correctly.
|
||||
External references from plugins are preserved during merges, so future imports can still recognize already-linked trails correctly.
|
||||
|
||||
## What Happens During a Merge
|
||||
|
||||
|
||||
83
docs/src/content/docs/use/plugins.md
Normal file
83
docs/src/content/docs/use/plugins.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Plugins
|
||||
description: How to set up third-party provider plugins with wanderer.
|
||||
---
|
||||
|
||||
Plugins add optional functionality that is not built into the core application.
|
||||
Once an administrator has installed a plugin, it appears in the plugin settings
|
||||
page where users can configure and enable it.
|
||||
|
||||
Plugin installation and self-hosted connector trust settings are administrator
|
||||
tasks. See [Plugin installation](/run/installation/plugins) for runtime bundle
|
||||
and connector details.
|
||||
|
||||
## Strava Plugin
|
||||
|
||||
:::caution[A Strava subscription is required]
|
||||
With Strava's June 2026 Developer Program update, accessing the Strava API as a
|
||||
"Standard Tier" developer requires an active Strava subscription. Because each
|
||||
<span class="-tracking-[0.075em]">wanderer</span> user connects with their own
|
||||
Client ID and Client Secret, everyone using this plugin counts as a Standard
|
||||
Tier developer and is subject to this requirement.
|
||||
|
||||
- **New developers:** subscription required since **June 1, 2026**.
|
||||
- **Existing developers:** subscription required from **June 30, 2026**.
|
||||
- Active developers without a subscription are granted **3 months free** to
|
||||
transition — redeem the offer from your
|
||||
[Strava API settings dashboard](https://www.strava.com/settings/api).
|
||||
|
||||
Your personal data export and device/wearable integrations are **not** affected;
|
||||
only programmatic API access is. A free (non-subscriber) Strava account can no
|
||||
longer use this plugin once the transition period ends. For details see Strava's
|
||||
[Developer Program update](https://communityhub.strava.com/insider-journal-9/an-update-to-our-developer-program-13428)
|
||||
and [API FAQ](https://communityhub.strava.com/developers-knowledge-base-14/strava-api-faq-12906).
|
||||
:::
|
||||
|
||||
### Creating an App in Strava
|
||||
|
||||
Before integrating Strava with <span class="-tracking-[0.075em]">wanderer</span>, you need to create an API application in Strava. Visit [Strava's API settings](https://www.strava.com/settings/api) and follow the steps to create a new API application. Your setup should resemble the following:
|
||||
|
||||

|
||||
|
||||
### Setting Up the Plugin
|
||||
|
||||
1. Copy the **Client ID** and **Client Secret**.
|
||||
2. Go to the plugins page in <span class="-tracking-[0.075em]">wanderer</span>'s settings.
|
||||
3. Click the settings button for the Strava plugin.
|
||||
4. Enter your **Client ID** and **Client Secret**.
|
||||
5. Choose whether you want to sync routes, activities, or both.
|
||||
|
||||

|
||||
|
||||
6. Click **Save & connect**.
|
||||
7. You will be redirected to Strava's authorization page. Keep all checkboxes selected and click **Authorize**.
|
||||
8. You will then be redirected back to <span class="-tracking-[0.075em]">wanderer</span>.
|
||||
9. Toggle the plugin on. It is now active.
|
||||
|
||||
If you later change the Client ID or Client Secret, reconnect the plugin. Other
|
||||
settings can be saved without repeating the OAuth flow.
|
||||
|
||||
## komoot Plugin
|
||||
|
||||
The komoot plugin requires only your komoot username and password:
|
||||
|
||||
1. Open the komoot settings from the plugins menu.
|
||||
2. Enter your komoot credentials.
|
||||
3. Save the settings.
|
||||
4. Toggle the plugin on. It will become active immediately.
|
||||
|
||||
Your planned and completed trails will now sync with <span class="-tracking-[0.075em]">wanderer</span>.
|
||||
|
||||
## Hammerhead Plugin
|
||||
|
||||
The Hammerhead plugin requires your Hammerhead account details:
|
||||
|
||||
1. Open the Hammerhead settings from the plugins menu.
|
||||
2. Enter your Hammerhead email and password.
|
||||
3. Choose whether you want to sync planned tours, completed tours, or both.
|
||||
4. (Optional) Set an "ignore trails before" date to avoid syncing duplicates if your Hammerhead account is already connected to other services.
|
||||
5. Save the settings and toggle the plugin on. It will become active immediately after a successful login.
|
||||
|
||||
:::note
|
||||
This page still describes provider setup at a high level. Provider-specific details depend on the installed plugin's manifest and capabilities.
|
||||
:::
|
||||
Reference in New Issue
Block a user