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:
slothful-vassal
2026-06-22 15:00:44 +02:00
committed by GitHub
parent 2e3d8537b8
commit 485ec53f6d
196 changed files with 20947 additions and 5454 deletions

View File

@@ -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,

View 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
```

View File

@@ -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`.

View 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`.

View File

@@ -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:
![Strava API Application](../../../assets/guides/strava_api_app.png)
### 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.
![wanderer Strava Integration](../../../assets/guides/wanderer_integration_strava.png)
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.
:::

View File

@@ -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

View 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:
![Strava API Application](../../../assets/guides/strava_api_app.png)
### 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.
![wanderer Strava Plugin](../../../assets/guides/wanderer_integration_strava.png)
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.
:::