# Plugins Plugins let anyone extend a self-hosted TREK instance with new features — a dashboard widget, a full page, a photo/calendar integration — **without touching TREK's source code**. They are developed by third parties, distributed from a public registry, and installed at the instance owner's discretion. > [!IMPORTANT] > **Plugins run arbitrary code, isolated but real.** Every plugin runs in its own > sandboxed child process with only the permissions you approve. TREK does not > maintain, audit, or take responsibility for community plugins. Grant a plugin > only the access you'd trust it to have with your data, and prefer plugins > marked **Reviewed**. ## Plugin types A plugin declares one `type` in its manifest, which decides where it surfaces: | Type | Where it shows | Typical use | |---|---|---| | **widget** | On the dashboard — in the sidebar, as a boarding-pass style hero overlay (`slot: "hero"`), or docked into a **detail panel** inside the trip planner: the open place (`slot: "place-detail"`), day (`slot: "day-detail"`), or reservation (`slot: "reservation-detail"`) | At-a-glance info like flight status or weather; a detail-panel widget is scoped to just the one place/day/reservation you have open | | **page** | As its own entry in the top navigation | A full self-contained tool | | **trip-page** | As a tab **inside every trip planner** (alongside Plan / Transports / Files), scoped to the open trip | A tool that works against one trip at a time | | **integration** | Nowhere visible — registers into TREK via hooks (e.g. a photo provider, a calendar source, or a **notification channel**) | Feed data into existing TREK features | ### Notification channels An `integration` plugin can add a whole new **notification channel** — Gotify, Pushover, Telegram, anything that takes a message — alongside TREK's built-in email, webhook and ntfy. Once you install and enable such a plugin, switch its channel on in **Admin → Notifications**. It then appears as a new column in every user's **Settings → Notifications** matrix, and each user supplies their own credentials on the plugin's own settings page and picks per-event what they want pushed — exactly like a built-in channel. Two guarantees hold for a plugin channel specifically: - **It is user-scoped.** Admin-only events (like *version available*) always go out over your built-in admin channels, never a plugin's. - **The plugin never sees anyone's trips.** TREK renders the notification — in the recipient's language, deep link already built — *before* handing it over. The plugin receives that finished message plus that user's own credentials for its service, and nothing else. It is given no acting user, so the trip-reading APIs other plugins use are refused to it outright. ### Plugin settings actions A plugin can put **buttons on its own settings page** — a "Test connection", a "Sync now". Users find them under **Settings → Plugins**, beneath that plugin's fields. An action runs **as the user who clicked it**, so a "Test connection" checks *their* credentials and can never see anyone else's. TREK refuses any action the plugin didn't declare in its manifest. ### Trip-page plugins: placement and tab takeover A **trip-page** plugin normally adds its tab *after* the planner's built-in tabs. If it needs a more prominent spot — or wants to stand in for a feature it replaces — its manifest can shape the tab bar via `capabilities.tripPage`: - **`position`** pins the plugin's tab to a preferred slot (a 0-based index) instead of being appended at the end. - **`replaces`** lists core planner tabs to **hide** while the plugin is active — e.g. a plugin that supersedes the built-in bookings or files view can take that tab's place rather than sit beside it. The **Plan** tab can never be replaced: a trip always keeps its planner view, so a plugin can take over individual tabs, never the whole trip. Hidden tabs come back the moment the plugin is deactivated or removed — the takeover lasts only as long as the plugin is on. ## Enabling plugins The plugin system is **on by default** — the runtime and the **Admin → Plugins** tab are available out of the box. Installed plugins still have to be activated one by one, so nothing third-party runs until you turn a specific plugin on. To switch the whole system off, set the environment variable to `false` and restart: ```yaml environment: - TREK_PLUGINS_ENABLED=false ``` When it's off, the **Admin → Plugins** tab shows a "turned off (TREK_PLUGINS_ENABLED)" banner and nothing runs. Installed plugins stay on disk, deactivated and harmless, until you turn the system back on. ## The isolation model — what a plugin can and can't do Each active plugin runs as a **separate OS process**, started under Node's permission model (`--permission`) with filesystem reads scoped to just its own code: - It has **no** access to `JWT_SECRET`, the database connection, or any TREK secret — those are simply not reachable by its process. - It **cannot** open `trek.db`, write files, spawn child processes, use worker threads, or load native addons. Its own data lives in a separate SQLite file it reaches only through TREK. - It talks to TREK exclusively over an internal RPC channel, and TREK only answers the capabilities the plugin's manifest **declares and you approve**. An ungranted call is refused, not merely ignored. - The RPC channel itself is **sealed off from plugin code**: even though the plugin runs in a forked process, its raw IPC primitives (`process.send`, `process.on('message')`) are revoked before its code loads — just like `process.binding`. So a plugin can neither forge host messages (fake its route table, spoof another request's identity) nor eavesdrop on other in-flight requests. Every interaction is forced through the capability-checked SDK. - A page/widget's interface runs in a **sealed browser frame** that can't read your session cookie or touch the surrounding TREK page. - If a plugin crashes, hangs, or runs out of memory, only *its* process dies — TREK keeps running and can restart or disable it. This means the permission list you approve is a **real boundary**, not a label. It bounds *what* a plugin can touch, not its intent within a grant: a plugin you let read trips **and** reach a host could send those trips there. See [[Plugin Permissions|Plugin-Permissions]] for exactly what each permission grants. ## The Admin → Plugins panel A single panel with a segmented **Installed / Discover** switch at the top left, plus a toolbar: - **Search** — filters the current list by name/description (and author, in Discover). - **Type** filter — All / Widget / Integration / Page. - **Status** filter (Installed view only) — All / Active / Off / Update available / Error. - **Sort** — Name / Recently updated / Updates first. - **Upload** — sideload a plugin from a `.zip`/`.tar.gz` (see [Installing](#installing-a-plugin)). - **Rescan** — rediscovers the on-disk plugins directory **and** force-pulls the remote registry, bypassing the 30-minute server cache and GitHub's CDN so a just-published plugin (or update) shows up immediately rather than up to ~35 minutes later (see [Installing](#installing-a-plugin)). Each installed row shows an icon tile with a **health dot** (green = active, blue pulse = starting, red = error, amber = disabled/incompatible, faint = inactive), the name and version, a **Reviewed** shield if applicable, a **Sideloaded** tag for manually-uploaded plugins (see [Installing](#installing-a-plugin)), and **capability chips** derived from its declared permissions — "Reads your trips", "Reads costs" / "Writes costs", "Dashboard widget", "Real-time updates", "Provides photos", outbound hosts, and so on — so a plugin's real reach is legible without opening anything. ## Reviewing a plugin before install In **Discover**, click any card to open its **detail modal**. This is where you review a plugin *before* it touches your instance. It fetches the plugin's live manifest (at the reviewed commit) and lays out: - **What it can access** — the permission-derived capabilities in plain language. - **Connects to** — the outbound hosts it declared (`egress`). - **Setup** — configuration fields it will ask for, with scope (instance/user) and whether each is required. - **Details** — version, download size, minimum TREK version, review date, plus links to the source repo and homepage. A **Reviewed** badge means a TREK maintainer scanned that exact version's source for malware — **not** that it works well or is harmless. It is not an ongoing guarantee. Read the access list and outbound hosts, not just the description. ## Installing a plugin Three ways, all from **Admin → Plugins**: 1. **From the registry.** In **Discover**, open a plugin and click **Install** (also available directly on the card). TREK downloads the pinned version, verifies its SHA-256 against the registry (and an author signature if the plugin ships one), safely unpacks it, re-validates the manifest, and registers it — **inactive**. Nothing runs yet. 2. **By upload (sideload).** Drag a plugin `.zip`/`.tar.gz` onto the panel, or use the **Upload** button. TREK extracts it into staging with the same hard guards as a registry install (slip/bomb-safe extract, strict manifest, no native binaries — only the registry SHA-256/signature checks don't apply, since there's no registry entry) and registers it **inactive**. A sideloaded plugin is marked **Sideloaded**, is unsigned and not registry-reviewed, has **no auto-update** and no "Source repository" link. Uploading over an existing id force-stops and deactivates the old code first, so the replacement never keeps running without a fresh activation. (Max 50 MB, same ceiling as the SDK's `pack`.) 3. **From disk.** Drop a plugin directory into the plugins code directory (`server/data/plugins`, or your `TREK_PLUGINS_DIR` volume) and click **Rescan** (or restart). TREK discovers it and registers it inactive. ## Activating a plugin Activation is the **toggle** on the installed row. Flipping it on grants the permissions you already reviewed and spawns the isolated process; flipping it off stops that process immediately while keeping the plugin's data. A page plugin then appears in the top navigation; a widget appears on the dashboard. There is no separate "Activate" button or second consent screen — you reviewed the permissions before installing. ## Dependencies between plugins and addons A plugin can require **other plugins** or **addons** to be present before it runs. This is enforced when you **activate** — installing never fails on a dependency, so you can always install first and resolve after. The installed row shows a plugin's dependencies as chips (amber when one is missing, out of range, or its addon is off). - **Requires an addon that's off** — activation is blocked with a message naming the addon. Turn it on in **Admin → Addons**, then flip the plugin on. Turning a required addon back **off** while the plugin runs disables the plugin automatically. - **Missing a plugin it depends on** — activation opens a **dependency dialog** listing what's missing; each has a **Download** button that fetches the newest compatible version from the registry (and its own dependencies) and then enables your plugin. A dependency that's installed but the *wrong version* shows an **Update** button instead. - **Depends on a plugin that's installed but off** — enabling your plugin **auto-enables its dependencies first**, in order. - **Disabling a plugin others depend on disables those dependents too** — a plugin can't keep running with a dependency that's gone. Plugins that declare a dependency can also **call each other's functions and exchange events** at runtime — always mediated by TREK, and only along a declared dependency (a plugin can't reach one it didn't declare). See [[Plugin Development|Plugin-Development#talking-to-other-plugins]]. ## Managing a plugin The **⋯** menu on each row: - **Restart** — stop and re-spawn the process (shown only while active). - **View error log** — the plugin's own crash/failed-request log. - **Allowed hosts** — add the hosts a plugin may reach, for a plugin that talks to a service only *you* can name. See [Allowed hosts](#allowed-hosts) below. - **Source repository** — opens the plugin's GitHub repo (registry installs only). - **Delete** — uninstalls: removes the code and lets you keep or delete its data. ## Allowed hosts A plugin's outbound hosts are normally fixed in its manifest, and you consent to them at install. But a plugin that talks to a **self-hosted service** — a Gotify, an ntfy, an Uptime Kuma — cannot know *your* hostname when it is published. Such a plugin declares `operatorEgress`, and you supply the hosts yourself. The plugin card shows a **"+ hosts"** chip when it works this way. Open **⋯ → Allowed hosts** and add the hostname (e.g. `gotify.mydomain.com`). TREK restarts the plugin so it picks up the new list. What this does *not* let anyone do: - **A plugin that never asked for it can never be given a host.** Only a plugin whose manifest declares `operatorEgress` is eligible, so the consent you gave at install still bounds what is possible. - **Only you — an admin — can widen egress.** An end user can never add a host, even for a plugin whose credentials they supply themselves. - Hosts are validated like manifest egress: no bare `*`, no whole-TLD wildcard, no scheme. Remove a host and the plugin loses it immediately (it restarts again). - Uninstalling drops the hosts, so a later plugin reusing the id can't inherit them. If the service runs on the **same machine or LAN** as TREK (a `localhost` or `192.168.x.x` address), you also need `TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on` — plugins may not reach private addresses by default. That relaxes the policy for *every* installed plugin, so only enable it if you trust them all. ## Reviewing what a plugin did — the activity log Every user can see exactly what plugins have done **in their name**. Under **Settings → Plugins**, the **activity log** lists every host-mediated action a plugin took while acting for you — across all plugins, newest first: each trip or cost it read, each place it wrote, each outbound call TREK made on its behalf. This is the user-facing half of TREK's tamper-evident (hash-chained) plugin audit: admins see the per-plugin view in **Admin → Plugins**, while this view is **never admin-gated** — anyone can review what was done with their own data. It's what keeps a plugin's deliberately broad read grants accountable to the person whose data is read. See [[Plugin Permissions|Plugin-Permissions]] for what each grant allows in the first place. ## Updating a plugin When the registry lists a newer version, an **Update to vX.Y.Z** pill appears on the row, and an **Update all** bar summarises how many are available. Updating swaps in the new code and, by default, transparently restarts the plugin on it. But if the new version declares **more permissions or new outbound hosts**, TREK installs the new code and **leaves the plugin off**, then shows a **re-consent dialog** listing exactly the new permissions and hosts. The plugin only runs again once you approve — an update can never silently widen what a plugin may do. Choosing "Later" keeps the new code installed but inactive. ## Building your own Plugins are built with the **plugin SDK** and its `trek-plugin` CLI. The CLI turns a built plugin directory into a publishable artifact and the ready-to-PR registry entry, so you never hand-compute a SHA-256 or hand-write registry JSON: | Command | What it does | |---|---| | `trek-plugin validate [dir]` | Runs the manifest + layout checks locally (a subset of registry CI, which additionally verifies the release, the artifact SHA-256, and the README over the network). | | `trek-plugin pack [dir] [--out plugin.zip] [--json]` | Builds `plugin.zip` in the installer's exact layout and prints its SHA-256 + byte size. Refuses native binaries; `docs/` is intentionally not shipped (the store fetches the screenshot from your repo). | | `trek-plugin entry --repo --tag [--zip z] [--merge entry.json] [--out f]` | Emits the registry entry — `commitSha`, `downloadUrl`, `sha256`, `size` and `minTrekVersion` (derived from the manifest `trek` range) all filled in. `--merge` prepends the new version onto an existing entry for updates. | | `trek-plugin release [dir] --repo --tag ` | The one-shot: `pack` → create the GitHub release → print the entry. | Run them via `npx trek-plugin-sdk …`. See [[Plugin Development|Plugin-Development]] for the SDK and manifest, and [[Publishing a Plugin|Plugin-Publishing]] for the registry PR flow. Any unique slug works — only `registry`, `install` and `rescan` are refused (they'd collide with admin API routes) — and an `id` stays bound to the GitHub owner who first registered it, so nobody can repoint an existing plugin. Entries may optionally carry an author signing key (`authorPublicKey` + a per-version `signature`) for offline signature verification on top of the SHA-256 pin.