Releases: liketrek/TREK
Release list
v3.4.0
The plugin platform matures, and Costs goes properly multi-currency. A plugin can now deliver its own notifications (Gotify, Pushover, Telegram…), declare the exact TREK versions it runs on, carry a signature TREK verifies and badges, ship an icon and its own settings page — and
trek-pluginbecomes a guided path from scaffold to published. Costs gets an editable trip currency that re-anchors the whole budget, per-currency settle-up payments, and totals, place prices and the PDF that stop pretending everything is EUR. Plus AirTrail multi-leg imports, coordinate-first Atlas, an offline Help centre that ships with the build, Catalan as language 23, and a broad reliability, mobile and security pass.No breaking changes — every new capability is opt-in, and all migrations run automatically on startup.
The team is growing — and TREK has a new home
Two things beyond the changelog:
- A new maintainer. @subdee (Konstantinos) joins @jubnl and me steering TREK — and he's already all over this release. And @fbnlrz (Fabi), who builds the plugin-dev skill, is stepping in to help with community management. Thank you both. 🙌
- TREK now lives in its own GitHub organisation: github.com/liketrek. The repo is now
liketrek/TREK— existing links and clones keep redirecting, so nothing breaks, but that's the new home.
Features
- A plugin can register a notification channel — the four built-in channels (in-app, email, webhook, ntfy) were a closed set, so a plugin could produce a notification but never deliver one. A plugin now declares
hooks.notificationChannel+hook:notification-channel; email/webhook/ntfy become providers behind a channel registry, and the hook runs userless (the host passes the recipient's decrypted settings in asconfig), so a channel plugin gets a push token without gaining the right to read that user's trips. - AirTrail connecting flights import as one multi-leg booking — layovers were inexpressible (every flight came in as its own reservation, and the connection country counted as visited in Atlas). The picker joins chains by airport continuity within 24h, and a joined booking imports detached from sync, since AirTrail has no multi-leg entity to round-trip to. Closes #1535
- A plugin declares the TREK versions it supports, and the host enforces it — the manifest's
trekis now a semver range, not a lower bound that cannot express "stops working at 4.0".install/host-compat.tsis the single answer for install, sideload, dev-link and activation; prereleases coerce to their target release, and an unparseableAPP_VERSIONwarns rather than blocking. Discover badges an incompatible entry and offers the newest version that does fit. - Plugin signature status, and a scoped re-trust override — signatures were verified and TOFU-pinned but never surfaced: an unsigned plugin looked identical to a signed one, and a refused update died in a toast. Refusals now carry codes, persist on the plugin row, and are badged. Only
SIGNATURE_KEY_CHANGEDis overridable (keys rotate; bad bytes don't), re-trust re-pins and updates in one call, the artifact must still verify under the new key, and both fingerprints go to the audit log. - A plugin can declare an icon — a lucide icon name in the manifest, resolved in one place (
PluginIcon.tsx) and rendered everywhere a plugin appears, instead of a genericBlockstile everywhere. Validation warns rather than errors, since the SDK's icon-name snapshot can legitimately trail the host. - The plugin CLIs warn when the SDK is out of date — the entry format, validator and permission catalog move with the host, so a stale SDK can
packandsubmitsomething today's registry CI rejects. Backed byupdate-notifier, so it never blocks on the network, and it stays silent in CI and off a TTY. rawBodyBase64onPluginRequest— a webhook author needs the exact bytes the sender signed; re-serializing the parsedbodywill not reproduce them. The host already set it onauth:falseroutes; it's now part of the public type.- The SDK mock host can drive settings actions and notification channels —
hook()alone can't express them, so a plugin could pass its tests and still break in production.app.action()models a button as user-initiated,app.channel.send/testmodels the channel as userless withconfigpassed in. - The trip currency is editable, and changing it re-anchors the budget — the trip currency is the base every expense and settle-up is netted against, but the only picker for it lived in the legacy Budget addon panel, so on the Costs panel a trip was stuck with whatever it was created as. It's now a field on the trip form (create and edit, gated on
trip_edit); the REST and MCP write paths already accepted it. Changing it isn't a rename, though: a frozenexchange_rateis "units of the row's currency per 1 trip currency" andcurrency = NULLmeans "the trip's own", so both are relative to the outgoing base.rebaseTripCurrency()runs first, pinning the implicit rows to the outgoing currency and re-freezing every rate against the incoming one — no stored amount is rewritten, and each expense keeps its real-world value across the switch. - Settle-up payments carry their own currency — paying a rouble debt in euros is normal, and the server has stored a
currency+ frozenexchange_rateon every transfer since #1445, but the UI never let anyone choose one, so a payment silently inherited whatever the viewer's display currency happened to be. The payment modal now has a picker (and reopens a payment in the currency it was recorded in), and a foreign payment shows in the ledger as$30.00 → 27,00 €like a foreign expense does, instead of stamping the display currency's symbol onto the raw stored number. - The display currency is optional, falling back to the trip's — Costs already resolved
default_currency || trip.currency, but the setting could never actually be empty: the store seeded'USD', so a user who had never touched it silently had every trip converted into dollars. The picker now leads with a Trip currency option, so an unset preference means "show each trip in its own currency" instead of forcing them all through one code. An explicit empty value persists and beats the admin-set instance default — it's a deliberate choice, not an absence. This also brings the public share's fallback to life: the payload has resolved sharer's currency → trip currency since #1361, but that branch was unreachable while every owner had a currency forced on them. - The in-app Help pages are served from the wiki that ships with the build —
/helpfetched its markdown fromraw.githubusercontent.comat runtime, so a self-hosted install was shown docs frommainrather than the version it was actually running, and help broke without network access. It now reads the repo'swiki/from disk, with no copy or build step — one__dirnameanchor resolves in dev, a source install and Docker alike. GitHub survives only as a fallback for when that directory can't be found: a page missing from a present wiki is a genuine 404, since falling back per-file would reintroduce the version skew this removes. Shipping it took two Docker edits, not one —.dockerignoreexcludedwiki/outright, so theCOPYalone would have produced an image with no wiki, quietly masked by the fallback.TREK_WIKI_DIRis an off-by-default escape hatch for serving your own docs. Costs ~30 MB of image size. - Booking routes show per trip, with an account default and a bulk toggle — a new always show booking routes setting seeds any trip whose routes were never touched, and a show-all/hide-all button in the day-plan toolbar flips a trip explicitly. The per-leg toggle still overrides in either mode.
- A plugin can ship its own settings page —
capabilities.settingsUi: trueframes the plugin'ssettings.htmlas a card under Settings → Plugins, in the same sandbox and postMessage bridge as its widget. Hosts that predate the flag strip it at install, so old instances simply don't show the card. - Maps open framed on their places — a trip in Japan opened on a world view at 0,0 and then flew across the planet; each renderer now derives its opening camera from its places at construction. The home-city default centre/zoom settings are dropped, since nothing reads them anymore.
- MCP exposes public transit planning tools — search and journey creation in parity with the web app, accepting every mode the provider can actually return (the old whitelist silently dropped itineraries with e.g. an AIRPLANE leg) and reporting a
droppedcount instead of conflating bad provider data with "no routes exist". - A visited Atlas region can be hidden, cascading to the country — a new
hidden_regionstombstone mirrorshidden_countries; hiding a country's last visible region hides the country too, and re-marking clears both. - Catalan (ca) is a supported language — the full locale, at strict key parity with
en.
Fixes
- Every plain-HTTP request a plugin made was refused — Node normalises
net.connect()args into an array, which undici's HTTP (not TLS) connector passes through;classifyConnectreadhostoff it, gotundefined, and fell back tolocalhost, so a declared public host was rejected as "not in the plugin's declared hosts". Failed closed, so never a hole, and unnoticed only because the one shipped egress plugin uses HTTPS. TREK_PLUGIN_ALLOW_PRIVATE_EGRESScould never have worked — the guard reads it inside the plugin child, whose env the supe...
v3.3.0 — 10,000 stars 🌟
TREK just crossed 10,000 stars on GitHub, and it's honestly still surreal to write that. Thank you — to everyone who starred the repo, opened an issue, shipped a plugin, translated a string or sent a PR: this is yours as much as mine. TREK isn't just growing, it's accelerating, and the goal has never changed: not just the best open-source trip planner, but the best trip planner, full stop. We're closing that gap, fast. Onwards ❤️
📣 We're looking for another maintainer
TREK is growing faster than @jubnl and I can carry on our own. At this pace the workload just isn't sustainable for two people anymore. So we're looking for one more maintainer to help steer the project.
You'd be a great fit if you:
- Know TREK's architecture well — or can get deep into it fast.
- Have built your own projects before — ideally software of this kind, maybe even professionally.
- Can write and understand code independently. AI assistants (Claude Code and the like) are absolutely welcome — but you genuinely need to understand the code you ship, not just paste it.
Interested? Email report@liketrek.com or reach Maurice on our Discord. We'd love to hear from you. 🙌
Plugins go from "can look at TREK and write a little" to "can automate TREK and connect it to the outside world" — full read/write symmetry across the planner, host-brokered notifications, AI, OAuth and inbound webhooks, plugin-provided notification channels (Gotify, Pushover, Telegram…), host-rendered hooks in tables, map, PDF, atlas and journal, and every call visible in a per-plugin activity log. Plus fixes across costs, calendars, transit, journeys and the planner. No breaking changes — every new capability is opt-in and admin-consented; existing plugins keep working.
Plugins: 3.2.0 vs 3.2.1 vs 3.3.0
Please consult the wiki for more information. Check out the “trek-plugin-dev” by @fbnlrz. It provides an AI agent with the entire SDK, the authorization model, the registry, and the one-click publishing workflow, so you can go from an idea to a published plugin in a single conversation.
| Capability | 3.2.0 | 3.2.1 | 3.3.0 |
|---|---|---|---|
| Usable permissions | ~8 | ~18 | 55 (+ http:outbound:<host> wildcard) |
| Write / mutate methods | 1 (costs.create) |
~16 | 62 — + reservations, accommodations, packing + bags, files, collab, tags, todos, atlas, vacay, journal, collections, day notes, trips.create, … |
| Provider hooks | 0 (declared but dead) | 2 live | 7 live — + tableContributor, mapMarkerProvider, pdfSectionProvider, atlasLayerProvider, journalEntryProvider |
| Read subsystems | trips, costs, users | + packing, files | + collab, journal entries, atlas bucket list, file content, exchange rates — 17 read surfaces |
| Event subscriptions | — | name + trip only | + {entity, entityId} hint + grant-scoped entity snapshot |
| Entity metadata | — | db:meta on trips/places/days |
+ reservations, accommodations |
| UI extension points | slots sidebar/hero, pages |
+ trip-page type, + place-detail slot |
+ day-detail/reservation-detail slots, core-tab replacement, tableContributor in 8 views, map-marker/PDF-section/atlas-layer/journal-entry render hooks |
| Host-mediated brokers | — | — | notify, AI, OAuth client, inbound webhooks, per-user settings, exchange rates |
| Notification channels | 4 built-in, hardcoded | 4 built-in, hardcoded | plugin-registered — Gotify, Pushover, Telegram, … as a real column in Settings → Notifications |
| Distribution & DX | registry install | + sideload .zip, typed SDK, design kit |
+ download counts, dev-link hot-reload, SDK 1.4.0 (motion, bridge), validator parity |
| Safety & ops | sandbox | + egress control, capability hardening | + per-plugin RPC rate limit, lifecycle recovery, uninstall secret purge, user activity audit log |
Plugin platform
- Read symmetry: plugins can now read collab notes/polls/messages, journal entries, atlas visited + bucket list, vacay, day notes, collections, categories, tags, todos, the trip roster, weather and exchange rates — plus file content behind its own
db:read:files:contentscope;tripsgainsgetDays/getReservations/getAccommodationsand cross-trip reads - Write expansion to 62 methods: reservations, accommodations, packing incl. bags (broadcasts respect per-user privacy, #858), files (attach/link/update/soft-delete), collab content, tags, todos, atlas, vacay, journal entries, collections, day notes, gated member-add and
trips.create— each still gated by the acting user's own permissions - 5 new provider hooks live:
tableContributor(host-rendered columns and actions in 8 views — reservations, transports, places, day, costs, packing, files, todos),mapMarkerProvider(trip-map overlay),pdfSectionProvider,atlasLayerProvider,journalEntryProvider - New UI surface:
day-detailandreservation-detailwidget slots;trip-pageplugins can replace core planner tabs and pick their position; page plugins show up in the mobile bottom nav - Events carry an
{entity, entityId}hint and a grant-scoped entity snapshot — filtered per plugin at delivery, never for private packing items or deletes - Entity metadata (
db:meta) extended to reservations and accommodations (5 entity types) - Host-mediated brokers, all new:
notify.send,ai.complete/ai.extract(host-brokered LLM access), an OAuth client (oauth.getToken+ callback flow), trusted inbound webhooks with a signature-header allowlist, a per-user settings form +ctx.settings, andrates.get - Background jobs: declared jobs now run on schedule (
jobs:run) - Settings-page actions: a plugin can put buttons on its own settings page (Test connection, Sync now) — user-initiated, so they act as the person who clicked
- Emoji hygiene: emojis in plugin-provided text that TREK renders in its own chrome (badges, table columns, trip warnings, map-marker and calendar labels, PDF sections, notifications) are stripped at the render boundary, so plugin output matches TREK's lucide icon language — plugins use the declarative
iconfield for glyphs; a plugin's own sandboxed/uiframe is untouched
Notification channels
- A plugin can now register a notification channel (
hook:notification-channel) — Gotify, Pushover, Telegram, anything that takes a message — alongside the built-in in-app / email / webhook / ntfy. It shows up as a real column in every user's Settings → Notifications matrix, each user supplies their own credentials on the plugin's settings page, and per-event opt-outs work exactly as they do for a built-in - The four hardcoded channels became a registry: email/webhook/ntfy are now providers wrapping the existing send functions (no delivery logic rewritten), and the event text is rendered once per recipient instead of once per channel
- Enabling the plugin is the opt-in — no separate admin toggle. Admin-scoped events (e.g. version available) are never deliverable to a plugin channel, and a plugin can't shadow a built-in channel id
- The hook runs userless: the host resolves the recipient's decrypted per-user settings and hands them to the plugin, so a channel plugin can be given your push token without being able to read your trips as you
Plugin security & operations
- Operator-supplied egress hosts (
operatorEgress): a plugin's allowed hosts are fixed in its manifest at publish time, which can't work for a self-hosted service — a community Gotify/ntfy plugin cannot know your hostname. Such a plugin now declares the flag and an admin adds the real hosts post-install (Admin → Plugins → ⋯ → Allowed hosts). Only an admin, never an end user; same validation as manifest egress; dropped on uninstall. Discover shows a "+ hosts you add" pill so the reach is visible before install - Per-plugin RPC rate limit (token bucket) — a misbehaving plugin can no longer starve the host event loop; the log channel (
ctx.log, stdout/stderr) is throttled the same way, so awhile(true)log loop can't freeze the instance either - Plugin settings keys are validated at install and the stored config is parsed null-prototype — a field named
__proto__could previously resolve offObject.prototypeand report as configured for users who had configured nothing - Plain-HTTP egress worked for no plugin at all (the guard misread Node's normalized
net.connect()args and refused declared hosts as "localhost"); it failed closed, so it was never a hole. Fixed, andTREK_PLUGIN_ALLOW_PRIVATE_EGRESS— documented but wired to nothing, since the child's env is scrubbed — is now actually forwarded, so a LAN service is reachable when an operator opts in - Lifecycle hardening: install/enable/disable/crash paths recover cleanly
- Uninstall purges plugin secrets; cron leak fixed; slot filtering and manifest normalization tightened (audit follow-ups)
- Bridge path hardening and a frame CSP for plugin-served static assets
- User-visible plugin activity log — see what each plugin read, wrote and invoked
- Backups now include each plugin's data and code; restore stages them and swaps on the next boot. Staging is atomic and the archived DB is a consistent
VACUUM INTOsnapshot, so an interrupted or concurrently-written backup can't corrupt or silently delete a plugin's only data copy - Lifecycle races closed: a late load event can't resurrect a stopped plugin or double-fire its jobs; a re-enable during shutdown can't lose its DB handle; erasure drains coalesce; a restore quiesces running plugins even for older archives — no "ghost" plugins
- The SDK mock host and dev runner now match the real host's rules (userless jobs/events, member/notify/
db.tx/ws guards, validation), so a plugin behaves the same locally and in production
Securit...
v3.2.1
Plugins go from read-mostly observers to a real extension platform: they can now write to the itinerary, contribute native data, react to core events, add whole planner tabs, and match TREK's look — all behind the same sandboxed, permission-gated boundary. Plus budget, collections, files, guest and mobile fixes. No breaking changes — every new capability is opt-in and admin-consented; existing plugins keep working.
Plugins: 3.2.0 vs 3.2.1
Please consult the wiki for more information.
| Capability | 3.2.0 | 3.2.1 |
|---|---|---|
| Usable permissions | ~8 | ~18 |
| Write / mutate methods | 1 (costs.create) |
~16 — costs C/U/D, places C/U/D, days C/U/D, itinerary assign/unassign, trips.update, metadata C/U/D |
| Provider hooks | 0 (declared but dead) | 2 live — placeDetailProvider, warningProvider (+ invoke.hook transport) |
| Event subscriptions | — | events:subscribe (name + trip only) |
| Read subsystems | trips, costs, users | + packing, files |
| Entity metadata | — | db:meta (no schema fork) |
| UI extension points | slots sidebar/hero, pages |
+ trip-page type, + place-detail slot |
| Design & DX | minimal | UI design kit, window.trek.ui, typed SDK, Cookbook + example |
| Distribution | registry install | + sideload .zip, registry force-refresh |
| Plugin dependency | — | Inter plugin dependencies (for potential plugin libraries) |
| Addon dependency | — | Ensure specific addon is enabled |
Plugin platform
- Permission-gated planner write APIs (places, days, itinerary, trips), each gated by the acting user's own
*_editpermission (#1429) trip-pageplugins (a tab inside every trip) and aplace-detailwidget slot- Provider hooks wired:
placeDetailProviderandwarningProvider, also atGET /api/place-details/:idand/api/trip-warnings/:id - Core event subscriptions (
events:subscribe) — react toplace:*,day:*,budget:*,file:*, … - Entity metadata (
db:meta) — per-plugin key/value store on trips/places/days - Broader reads:
ctx.packing.list/ctx.files.list; costs read + write (get/list/create/update/delete) - TREK UI design kit (
<!-- trek:ui -->→ tokens, glass, dark-mode, accent, appearance flags,window.trek), native scaffold, themed dev preview, auto-height glassy cards - Author DX: typed
ctxreturns,window.trek.uiDOM helpers, Plugin Cookbook +trip-doctorexample,clack/promptsCLI - Sideload a plugin by uploading a
.zip; force-refresh the registry
Plugin security
- Open redirect prevented in the plugin route proxy
hook:*grant enforced — implementing a hook isn't enough without the consentpacking.listrespects per-user visibility (#858) — no leaking another member's private items- IPC parent/child sealed, arbitrary API access blocked, full capability surface hardened + tested
Budget & costs
- FX rate frozen on every cost and settlement write, so a later rate drift can't re-open a settled position (#1445)
- New "Outstanding amount" card
Collections, files & wallet
- Editing a saved place no longer resets its status to "idea"
- Save picker shows above the mobile place detail
- Apple Wallet
.pkpassessupported, incl. booking uploads and the files tab (#1447, #1448)
Guests
- Guest names are per-trip again — the same friend can be "Jake" on two trips instead of being auto-renamed to "Jake 2" (#1446)
Planner & mobile
- Drag & drop disabled on mobile so the places list scrolls
- Day-plan collapse state persists after closing the page
- Remove-from-day button is icon-only on mobile (#1432)
Other
- Keep stored settings when an update fails; Unsplash API key usage fixed
- Translation fixes (Vietnamese "Disabled", remaining new keys across 22 locales)
- Added a Code of Conduct
Full changelog: v3.2.0...v3.2.1
v3.2.0
TREK 3.2.0
TREK becomes a platform. A sandboxed Plugin System with a community registry, one-click installs and a full author SDK. Collections — a personal place library that lives outside your trips. Public transit routing right in the planner, no API key needed. AI booking import for the confirmations structured parsing can't read. On top: per-user Appearance & theming, a robust offline mode with real conflict resolution, video in journeys and files, subscribable calendar feeds, an in-app help centre — and Vietnamese as language number 22.
No breaking changes — all 19 database migrations run automatically on startup, and Collections and AI Parsing stay off until you enable them.
Plugin System
TREK is now extensible. Anyone can build a plugin — a dashboard widget, a full page, or a headless integration — and self-hosters can browse a community registry and install it with one click, without ever touching TREK's source. Plugins run with real OS-level isolation behind an explicit permission model, and the whole system ships with an author toolchain that takes you from scaffold to published registry entry in a handful of commands. (#1415)
- Three plugin types — widgets render on the dashboard (sidebar card or a
heroslot on the boarding-pass bar), pages get their own entry in the navigation, and integrations run headless server logic (API routes + scheduled jobs) - Real isolation, not a label — every active plugin runs as its own OS process under Node's permission model: it cannot read
trek.dbor any TREK secret, write files, spawn processes, or load native addons; its data lives in a separate per-plugin SQLite file - Capability permissions — a plugin declares what it needs (own database, membership-checked trip reads, public user profiles, namespaced WebSocket broadcasts, per-host outbound HTTP) and TREK's RPC host only exposes what was granted — an ungranted call is unreachable, not merely refused
- Egress guard with SSRF backstop — all outbound traffic (fetch, sockets, UDP, even DNS) is restricted to the plugin's declared hosts, and connections to loopback/private/link-local/metadata addresses are refused even for allow-listed hosts
- Sealed plugin UI — pages and widgets render in sandboxed, opaque-origin iframes with a locked-down per-plugin CSP; a plugin's frontend can never read your session cookie or the surrounding TREK page
- Registry with one-click install — Admin → Plugins → Discover browses the public TREK-Plugins registry; installing verifies the artifact's pinned SHA-256 (plus an optional Ed25519 author signature), safely unpacks it, and registers it inactive — nothing runs until you flip the toggle
- Review before you install — the detail view lists in plain language what a plugin can access, which hosts it talks to, and what it will ask you to configure, alongside a Reviewed badge for maintainer-scanned versions and an honest disclaimer about what the sandbox does and doesn't guarantee
- Re-consent on updates — an update that requests new permissions or new outbound hosts is installed but left off until you approve the exact delta; an update can never silently widen a plugin's access
- A real management surface — search, type/status filters, Update all, per-plugin health, error log, a capability audit log, restart, and uninstall with the choice to keep or delete the plugin's data
trek-plugin-sdkon npm —npx trek-plugin-sdk createscaffolds a plugin,devruns it locally against a mock host that enforces your manifest's permission grants exactly like the TREK host (no TREK instance needed; the OS sandbox and egress guard apply inside a real TREK), andpublishdoes the entire release — pack → GitHub release → preflight → auto-opened registry PR — in one command- Koffi, the reference plugin — TREK's mascot ships as a worked example: an animated suitcase hero widget driven by your real trip data, published in the registry
- On by default, with a kill switch — the plugin system is available out of the box, but installed plugins still have to be activated one by one, so nothing third-party runs until you turn a specific plugin on; set
TREK_PLUGINS_ENABLED=falseto switch the whole system off (installed plugins stay on disk, deactivated) - Hardened by adversarial audit — two dedicated audit passes (threat model: malicious plugin author, malicious artifact) before merge closed sandbox-escape, SSRF, decompression-bomb, DoS and consent-bypass findings
Docs: Plugins · Plugin Development · Plugin Permissions · Publishing a Plugin
Collections — A Personal Place Library
A place you saved used to exist only inside its trip. The new Collections addon gives every user a place library outside of any trip: keep named lists of spots you've discovered — a "Norway someday" wishlist, "Best coffee in Lisbon" — copy places into and out of any trip, and share lists with other users. Enable it under Admin → Addons to get the new /collections page. (#1081)
- Named lists — each with a colour, description, links and a cover image (upload or search Unsplash); a built-in All saved view unions your whole library
- Idea / Want to go / Visited — a per-place status you cycle with one tap, plus status and category filters
- List + map layout — lists rail, places list and a persistent map side by side, with a full-map toggle and a place detail sheet; mobile keeps a list/map switch
- Save from a trip — "Save to collection" in the place inspector and context menus, plus a bulk action that copies every selected trip place into a chosen list (duplicates are skipped)
- Copy to trip — saved places copy into any trip with name, description, category, notes, price, coordinates, photo and tags; the trip's add-place modal gains a Saved places picker sorted by proximity to the trip
- Bulk actions — select-all, bulk delete, bulk copy-to-trip, and move or duplicate a selection into another list
- Sharing with member roles — invite other users to a list (accept/decline, like Vacay fusion) with per-member roles: viewer, editor or admin, enforced on the server and synced live
- Dashboard widget — your lists as compact badges on the dashboard, hideable per user
- Custom labels — each list can define its own labels (e.g. Berlin, Hamburg, Ostsee in a "Germany 2026" list) to group its places. Create/rename/recolor/delete them in a label manager, assign them from a place's detail sheet or in bulk from the selection toolbar, and filter both the list and the map by label (multi-select, any-match). Labels are shared with everyone on the list — managing and assigning needs edit rights, filtering is open to all members
Full guide: Collections
Public Transit Routing
Google-Maps-style public transit directions, right in the planner — with no paid provider. Routing comes from Transitous, the community-run MOTIS service built on open GTFS feeds: free, open data, no API key required. (#1065)
- Automated transport mode — the Add-transport dialog gets a Manual / Automated switch; Automated embeds the transit search, and a new tram button on each day header opens it with that day preset
- Journey search — from/to stop search with the day's own places and hotels as quick picks, depart/arrive time, mode filters (train, subway, tram, bus, ferry, cable car) and ranking by best route, fewer transfers or less walking
- Readable results — itinerary cards with local times, duration, transfers and walking minutes, line badges in their official colors, and an expandable stop-by-stop breakdown with platforms, headsigns and operators
- First-class transit entries — a saved journey is its own reservation type: the day timeline renders the legs inline and folds the full itinerary out in place, and the Transports tab lists journeys in their own section
- Journey view — duration / transfers / walking stat tiles, inline title rename, notes with a markdown toolbar, and a "Change route" action that re-runs the search pre-seeded with the journey's endpoints
- Real paths on the map — journeys draw along the actual rail and bus alignments in each line's color, with walks as dotted connectors, on both map renderers
- Right times, right slot — times are station-local, transfer waits count toward the duration, and after-midnight arrivals land on the next trip day
- Self-host friendly — the server proxies Transitous (authenticated, rate-limited, cached); point
TRANSIT_API_URLat your own MOTIS instance for zero third-party egress
Also on the map: booking route endpoint labels now default to off for a calmer look — re-enable them anytime in Settings.
Docs: Transport: Flights, Trains & Cars
AI Booking Import
Booking import no longer gives up on documents KDE Itinerary can't read. The new opt-in AI Parsing addon adds an LLM fallback that extracts flights, trains, buses, ferries, rental cars, hotels, restaurants and event tickets from unstructured confirmations — with first-class support for self-hosted local models. Based on work by @jubnl. (#1301, #1318)
- Bring your own model — Local (Ollama), OpenAI — including any OpenAI-compatible endpoi...
v3.1.4
Changelog
- fix(backups): prevent recursion in path that is backed up
- fix(share): show user currency instead of the default euro in the share page
- fix(files): show descriptive error for unsupported upload type
What's Changed
Full Changelog: v3.1.3...v3.1.4
v3.1.3 — 8,000 stars 🌟
8,000 stars — thank you 🌟
TREK just passed 8,000 GitHub stars. For a self-hosted travel planner that started as a side project, that's genuinely humbling — and it's all of you: the bug reports, the translations, the pull requests, and everyone quietly running TREK on a NAS or a Pi in a closet somewhere.
A patch release is normally fixes only. As a small thank-you, 3.1.3 ships a few new features too — alongside the usual pile of fixes. Keep the issues and PRs coming. 💛
✨ New features
- Distance unit — metric or imperial. A per-user setting so distances render in kilometres or miles, mirroring the temperature unit. Default stays metric. (#1300 — thanks @mvanhorn)
- MapLibre + OpenFreeMap map provider. A third, fully self-hostable and API-key-free map provider alongside Leaflet/OSM and Mapbox. The GL engine is code-split, so Leaflet-only setups never download it. (#1317 — thanks @hykilpikonna)
- Places sidebar follows the map. Click a marker and the places list scrolls straight to it. (#1315 — thanks @hykilpikonna)
- Precise Google Maps links. The "Google Maps" button now resolves to the actual place via its feature ID instead of dropping a raw-coordinate pin. (#554 — thanks @hykilpikonna)
🔒 Security
This release also closes one privately-reported vulnerability. Updating is recommended.
| Advisory | Severity | Issue |
|---|---|---|
| GHSA-r4cp-666p-8f69 | Medium (CVSS 4.3) | Cross-trip reservation title disclosure — a member of one trip could reference another private trip's reservation id (on file upload, update or link) and read its title back through the file's reservation join. Cross-trip references are now rejected on every file write path. (CWE-639) |
Changelog
- feat: distance unit (metric/imperial) display setting (#1300)
- feat(maps): MapLibre + OpenFreeMap as a self-hostable map provider (#1317)
- feat(planner): seek the places sidebar to the selected map marker (#1315)
- feat(maps): precise Google Maps place links via feature IDs (#554)
- fix(map): drop the phantom hotel leg to/from a transport endpoint on arrival days (#1321)
- fix(map): draw the hotel-to-hotel leg on a transfer day with no activities (#1297)
- fix(map): pin the GL basemap label language to the UI language (#1299)
- fix(planner): show the route tools for a single place when optimizing from accommodation (#1330)
- fix(maps): make Overpass endpoints configurable and harden the POI search (#1309)
- fix(atlas): assign border places by polygon, not just bounding box (#1331)
- fix(reservations): keep dated bookings on their date when the trip range shifts (#1288)
- fix(costs): freeze the FX rate so settled expenses don't reopen when rates drift (#1335)
- fix(airtrail): import the airline name, not the ICAO code (#1334)
- fix(dashboard): persist the currency & timezone widgets so an upgrade keeps them (#1311)
- fix(places): guide single-place links to the right importer (#1304)
- fix(share): translate the day label on the public share page (#1296)
- fix(pwa): use the self-contained app icon for the favicon so it shows on dark tabs (#1328)
- fix(files): reject cross-trip reservation/place/assignment links — GHSA-r4cp-666p-8f69
- i18n: new Swedish translation + refreshed Dutch translations
- chore: replace the 3.0 upgrade notices with a thank-you / support modal
- docs(wiki): document all environment variables and fix the kitinerary install steps
What's Changed
Released by @mauriceboe in #1324, with community contributions from @hykilpikonna, @mvanhorn, @olsson82, @eindpunt, @Hardik-369, @jubnl and @CooperSheroy. Thank you all. 🙌
Full Changelog: v3.1.2...v3.1.3
v3.1.2
Changelog
- fix(budget): accept comma decimal separator in expense amounts
- fix(budget): scale category bars relative to top category
- chore: dockerignore spec.ts files
- fix(atlas): gzip-compress responses so large country GeoJSON loads behind reverse proxies (#1262)
- chore: add ca_profile.xml for Unraid Community Apps submission
- fix(security): allow same-origin PDF previews under CSP (#1253)
- fix(dashboard): count archived trips in travel stats (#1264)
- fix(days): align note time limit to 250 and keep toasts above modal blur (#1252)
- fix(pdf): show photos for OSM places in the trip PDF (#1130)
- fix(chart): allow setting storageClassName on PVCs (#1261)
- test(days): bump over-long-time assertion to the new 250 limit (#1252)
- fix(costs): move the unfinished marker to the category icon on mobile (#1266)
- fix(dashboard): add a text-shadow so spotlight and card titles stay legible (#1267)
- fix(atlas): give the country-GeoJSON fetch a longer timeout (#1254)
- feat(helm): add annotations support for PVCs (#1270)
- fix(admin): show non-Docker update steps when not running in Docker (#1269)
- feat(planner): bring back the Google Maps route export button (#1255)
- fix(auth): keep the last admin when OIDC claims would demote it (#1274)
- fix(dashboard): show an error instead of a blank trip list when the server is unreachable (#1283)
- fix(packing): keep a custom category when its last item is removed (#1289)
- fix(costs): allow recording an expense with no split or payer (#1286)
- fix(map): draw the route line to and from the day's accommodation (#1275)
What's Changed
- v3.1.2 bug fixes by @jubnl & @mauriceboe in #1258
Full Changelog: v3.1.1...v3.1.2
v3.1.1
v3.1.1 - Hotfixes
⚠ Proxmox VE community-script users: You can now update safely to 3.1.1!!
Changelog
- fix(shared-view): render each leg of multi-leg flights correctly
- feat(pdf): add legs to pdf export
- fix(demo): skip first-run admin seed in demo mode
- fix(docker): ship the encryption-key migration script in the image
- fix(vacay): keep the mode toolbar above the mobile bottom nav
- fix(dashboard): show the correct reservation date regardless of timezone
- fix(atlas): cursor-following tooltips and removing countries from search
- fix(oidc): keep dots in generated usernames
- fix(collab): show poll option labels in the UI
- feat(backup): make the upload size limit configurable
- feat(costs): create an expense from a booking, fix editing total-only items
- test(reservations): align syncBudgetOnUpdate unit tests with no-wipe + type-sync
- fix(planner): keep a reservation on its day when edited
- fix(planner): derive a booking day from its date when none is set
- fix(planner): let a booking's day follow its date when edited
- fix(atlas): keep the continent breakdown in sync on mark/unmark
- feat(admin): let admins set a default currency for new users
- fix(atlas): give every sub-national region a distinct code
- fix(dashboard): never crash on a malformed reservation date
- fix(airtrail): gate airtrail update behind a user setting, on airtrail update: rebuild payload from fresh data to prevent any data loss
- fix(costs): rework the cost panel UX wise and apply prettier on the shared package
- fix(airtrail): don't use cabin class as seat on import
- fix(airtrail): import scheduled flight times instead of actual
- fix(planner): hydrate per-assignment times when editing a place from the pool
- fix(mcp): make write tools return client-valid, hydrated entities
- fix(mcp): fix ICS/maps/accommodation bugs, add settlement & template tools
- chore: bump nodemailer
- chore: bump multer
What's Changed
- v3.1.1 bug fixes by @mauriceboe & @jubnl in #1228
Full Changelog: v3.1.0...v3.1.1
v3.1.0
TREK 3.1.0
TREK 3 — rebuilt from the inside out. A modern NestJS backend, a React 19 frontend, and an end-to-end shared Zod contract layer so the client and server can never drift. On top of that foundation: a Splitwise-style Costs rework, Passkey login, AirTrail flight sync, multi-leg flights, real road routing, a reworked dashboard, five new languages, and a comprehensive offline/PWA hardening pass.
This release also fixes 18 privately-reported security vulnerabilities — including a critical authentication bypass. Updating to 3.1.0 is strongly recommended for every self-hosted instance. See Security below.
⚠ Proxmox VE community-script users: please wait before updating to v3.1.0
v3.1.0 changes how TREK is built and run:
- the repo is now an npm workspaces monorepo (client + server + new shared package) with a single root lockfile,
- the server is now compiled (dist/) instead of run directly via tsx,
- there are new build/runtime requirements (e.g. kitinerary-extractor for booking import).
The community-scripts (https://community-scripts.org/scripts/trek) LXC helper for TREK still targets the old 3.0.x layout. If you run its Update option right now, the build will fail and may leave your container in a broken state.
What to do: stay on your current version for now. An updated script is being submitted to ProxmoxVED and will roll out to ProxmoxVE after review — we'll post here once it's live. Your data (/opt/trek/data, /opt/trek/uploads) and .env are safe in the meantime.
Docker, Unraid, Helm and manual installs are not affected.
TREK 3 — NestJS + React 19
The biggest architectural change in TREK's history. The Express backend has been migrated to NestJS, the frontend upgraded to React 19, and both halves now share a single Zod contract package as the source of truth for every request and response.
- NestJS backend — modular architecture with dependency injection, replacing the previous Express app (#1087, foundation harness #1050)
- React 19 frontend — upgraded across the whole client
- Shared Zod contracts — one typed, validated schema package consumed by both client and server, so API shapes stay in lockstep
- Monorepo build pipeline —
sharedis now a properly built package; i18n moved into it so every surface reads from one translation set (#1056, #1066) - Pilot-then-migrate — the cutover started with a single module (
/api/weather) to prove the harness before the full migration (#1053)
Migrations run automatically on startup — no manual steps.
Security — Update Strongly Recommended
This release closes 18 privately-reported vulnerabilities, disclosed through coordinated private reporting. They range from a critical authentication bypass down to low-severity hardening. All self-hosted instances should update to 3.1.0.
Critical
| Advisory | Issue |
|---|---|
| GHSA-mjh4-w6fq-54qm | MFA bypass — the pre-2FA mfa_token was accepted as a full session token (JWT purpose confusion) |
High
| Advisory | Issue |
|---|---|
| GHSA-fvgw-r58q-4cw4 | OIDC account takeover — an SSO identity could be linked to an existing local account without email_verified |
| GHSA-627x-pmfq-98qv | SQL injection via unvalidated column names in journey entry update |
Medium
| Advisory | Issue |
|---|---|
| GHSA-xmqx-m462-52qr | OIDC login CSRF / session fixation — state was not bound to the initiating browser |
| GHSA-6vv9-669j-468p | Arbitrary file deletion under /uploads via attacker-controlled trip cover_image |
| GHSA-mggj-xgv8-95fp | MCP share-link tools let a non-owner member mint a public trip share link |
| GHSA-jx25-7cj6-88hq | Cross-trip IDOR — budget member paid-flag toggle not scoped to the trip |
| GHSA-p7pv-2jpm-qjx3 | Journey share permission flags (timeline/gallery/map) enforced only client-side |
| GHSA-pfr2-fgv9-q5xv | Stored HTML injection via reservation endpoint labels rendered into map markers |
| GHSA-h66w-m5g2-cqpc | Path traversal in the public journey photo proxy (local provider) reads auth-gated uploads |
| GHSA-3frp-pcr3-w932 | Stored HTML injection via journey entry story in the PDF export |
| GHSA-24x9-fcj9-vp6w | IDOR in the journey photo proxy — a share token resolves arbitrary asset IDs |
| GHSA-6f5j-xgjg-h9q7 | MCP tools bypass the admin-configurable RBAC permission system |
| GHSA-rrrf-7cf6-8736 | A settings password change did not invalidate existing sessions / tokens |
| GHSA-f5vh-p2h5-x735 | Blind SSRF via unvalidated redirect-following in Google/Naver list import and Maps URL resolution |
| GHSA-mx6m-qxv8-w624 | IDOR — any authenticated user could read another user's journey share token (full journey leak) |
Low
| Advisory | Issue |
|---|---|
| GHSA-w6f3-fp9r-xp6p | Admin backup restore extracted archives without a decompression-size limit (zip bomb) |
| GHSA-g8rf-gqrw-4qf9 | Stored cross-user HTML injection via trip title in the Journey suggestion banner |
Reported through coordinated private disclosure by @bytejmp, @tonghuaroot, @therawdev, and @shukla304. Thank you for reporting these responsibly.
Costs — Splitwise-Style Expense Splitting
Budget has been reworked into Costs: a proper shared-expense tracker built for group trips.
Please note: Due to the major transition from a budgeting tool to a cost tracker, previous settings in the budget add-on (e.g., cost splitting) cannot be automatically carried over and get a reset. All entries will be retained in the new add-on but will be assigned to the "Other" category.
- Splitwise-style splitting — record who paid and split a cost across trip members, with running per-person balances
- Settle up — one-tap minimal-transfer settlement with persisted history and undo
- Multi-currency — log expenses in any currency (#551)
- Multi-payer support — costs aren't tied to a single payer
- Fixed categories — a consistent category set for cleaner reporting
- Mobile-first — redesigned for phone use end to end
Passkey (WebAuthn) Login
- Passwordless sign-in — register a passkey and log in with Face ID / Touch ID / a security key (#297, #1111)
- WebAuthn standard — platform and roaming authenticators supported
- Coexists with existing auth — passkeys sit alongside password, SSO, and MFA
Authentication
- Remember me option — Add a Remember me checkbox on login screen to extend session expiry (#1189)
AirTrail Integration
Two-way flight sync with AirTrail (#214, #1158).
- Import flights — pull your flight history from AirTrail into TREK
- Two-way sync — flights flow in both directions and stay aligned
- Reservation-aware — imported flights land as transport reservations
Planner & Maps
- Real road routes (OSRM) — when route display is enabled, day routes follow actual roads with travel-time connectors instead of straight-line estimates; the hidden route-calculation setting is gone, so enabled routes always use real road geometry (#1060, #1064)
- Multi-leg (layover) flights — model connecting flights with layovers as a single booking (#872, #1146)
- Reorder whole days + insert a day — drag entire days around and insert a new day into a trip (#589, #1148)
- Explore places on the map — discover and add points of interest (OpenStreetMap / Overpass) directly from the planner map; instance-wide Mapbox token support (#1147)
- More transport types — bus, taxi, bicycle, ferry and others (#1105)
- Richer imported places — list-imported places are enriched via the Places API (#886, #1161)
- Route optimization around accommodation + confirm-before-delete on notes (#1123)
- Category-coloured markers — map marker borders reflect the place's category colour (#1062)
- Mobile route tools — planner route tools are reachable from the mobile day-plan sheet (#1142)
Reservations
- Native booking-confirmation import — drop in a booking confirmation and TREK extracts the details via KDE KItinerary (#1102)
Dashboard Rework
- Boarding-pass hero — the next trip rendered as a boarding-pass-style h...
v3.1.0-pre.3
⚠ PRE-RELEASE — USE AT YOUR OWN RISK ⚠
This is a pre-release build of TREK. It may contain bugs, incomplete features, or breaking changes. It is not recommended for production use.
- Do not use this on a live instance without a full backup
- Database migrations may not be reversible
- APIs and configuration options are subject to change without notice
If you encounter issues, please report them at https://github.com/mauriceboe/TREK/issues.
Stable releases are available on the main release page.
⚠ Pre-release image — not recommended for production use.
| Tag | Description |
|---|---|
3.1.0-pre.3 |
Pinned to this exact pre-release build |
3-pre |
Latest pre-release on the v3 line |
latest-pre |
Latest pre-release across all versions |
docker pull mauriceboe/trek:3.1.0-pre.3What's Changed
- feat(auth): add "Remember me" checkbox to extend session lifetime by @jubnl in #1189
- fix(backup): restore from Docker, fail-fast on shadowed /app, bundle encryption key (#1193) by @jubnl in #1197
- fix(places): fall back to search when autocomplete details lookup fails (#1192) by @jubnl in #1198
- fix(planner): scroll long place description/notes on mobile (#1195) by @jubnl in #1199
Full Changelog: v3.1.0-pre.2...v3.1.0-pre.3