Four separate defects sat behind "offline mode is broken". The app shell. On the first online launch after a version bump, App.tsx deleted EVERY Cache Storage bucket and unregistered EVERY service worker, then reloaded. That is exactly wrong for a Workbox autoUpdate build, which swaps its precache atomically on install and never leaves an empty one. Between the purge and the completion of a fresh 22 MB precache the origin had no shell at all; anyone who closed the app or lost signal inside that window could no longer start the PWA offline. It also threw away the map tiles and documents the user had downloaded on purpose. It now asks the worker to fetch the new build and reloads once it has taken over. The version marker also moves inside the guarded path, because it was written after the purge and outside its try: a throwing setItem left the caches gone, the marker unwritten and the reload unreached, repeating on every launch. The reported version is validated before it reaches this device's storage and before it is compared against the stored marker, so a value that is not a release tag can neither be persisted nor start an update on every launch. The session. loadUser's offline branch kept the persisted session but never called onAuthSuccess, the only caller of setAuthed(true), and the mutation queue's flush, syncAll and prepareForOffline are all gated on it. A session that launched without network could therefore never sync again for the rest of its life, even after the signal returned: the offline settings buttons spun and returned instantly having done nothing, and queued edits never uploaded. It also left reads pointed at the anonymous database instead of the user's own. The date rule. A trip is only cached when it has not ended, and one that ended over a week ago is actively evicted. The per-trip switch was an opt-out list, so ticking a finished trip changed nothing at all. For anyone whose trips are all in the past, "Download for offline use" stored none of them, reported success, and the cache stayed at zero. Switching a trip on now pins it, which overrides both the skip and the eviction, and the switch shows the honest position rather than promising storage that never happens. What the screen says. A sync that never started (already running, offline, signed out) was indistinguishable from one that stored everything: the progress bar painted full and a green "Ready for offline use" appeared over a run of nothing. Both syncs now report why they stopped and the screen says so. A rejected Dexie read no longer renders as "No trips cached yet", and the clear button stays reachable when the database holds data that no bookkeeping row points at. A run that rejects outright says so too: the trip list it starts from is a plain fetch, so a network blip took the whole thing down and left the last progress frame on screen looking like a finished download. The 401-with-HTML branch in the API client now confirms a reachable proxy before tearing the worker down, since several of TREK's own routes answer 401 with a text/html body. Both settings shells move onto one shared hook rather than growing the same logic twice, and the tile prefetch caps its depth when the browser refused persistent storage. Opaque tiles are billed at about 7 MB each, and an origin that is not exempt from eviction loses its whole bucket, precached shell included, which Workbox then never refills because sw.js has not changed. Closes #2228
A self-hosted, real-time collaborative travel planner — with maps, budgets, packing lists, a journal, and AI built in.
What you get
See all features
Most of what follows is an addon an admin switches on or off. Lists, Costs, Documents, Collab, Vacay and Atlas ship on; Journey, Collections, MCP, AI Parsing and AirTrail ship off and are marked below.
🧭 Planning
|
🧳 Bookings and money
|
👥 Collaboration
📔 Journal, Atlas and Vacay
|
🧩 Plugins
🤖 AI and MCP
|
📱 Mobile and offline
|
⚙️ Admin, accounts and security
|
AI usage
We use LLM-assisted coding tools across parts of this codebase. Nothing ships that a maintainer has not read and understood: every change goes through a pull request, is reviewed and tested, and has a human who can answer for it. "The AI wrote that" is not an answer any of us would accept from ourselves.
See How we use AI in TREK for the details.
Get started in 30 seconds
ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d -p 3000:3000 \
-e ENCRYPTION_KEY=$ENCRYPTION_KEY \
-v ./data:/app/data -v ./uploads:/app/uploads mauriceboe/trek
Open http://localhost:3000. On first boot TREK seeds an admin account — if you set ADMIN_EMAIL/ADMIN_PASSWORD those are used, otherwise the credentials are printed to the container log (docker logs trek).
Tech stack
Real-time sync via WebSocket (ws). Backend on NestJS 11. State with Zustand. Auth via JWT + OAuth 2.1 + OIDC + Passkeys (WebAuthn) + TOTP MFA. Weather via Open-Meteo (no key required). Maps with Leaflet and Mapbox GL.
Docker Compose (production)
The repository ships a ready-to-use docker-compose.yml
with secure defaults and every option documented inline. Download it, then:
docker compose up -d
See Install with Docker Compose for the full walkthrough.
Helm (Kubernetes)
helm repo add trek https://chart.liketrek.com
helm repo update
helm install trek trek/trek
See charts/README.md for values.
Install as App (PWA)
TREK works as a Progressive Web App — no App Store needed.
- Open TREK in the browser (HTTPS required)
- iOS: Share ▸ Add to Home Screen
- Android: Menu ▸ Install app (or Add to Home Screen)
TREK then launches fullscreen with its own icon, just like a native app.
Updating
See Updating — Docker Compose, Docker run, Helm, Portainer, Unraid and Proxmox, plus the encryption-key note.
Reverse Proxy
For production, put TREK behind a TLS-terminating reverse proxy. TREK uses WebSockets for real-time sync, so the proxy must support WebSocket upgrades on /ws.
If you use the MCP addon, the proxy must also pass the Mcp-Session-Id header through in both directions on /mcp — Nginx and Caddy do this by default, but a proxy that strips it makes every tool call open a new session instead of reusing one. See the Reverse Proxy wiki page for details.
Nginx
server {
listen 80;
server_name trek.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name trek.yourdomain.com;
ssl_certificate /etc/ssl/fullchain.pem;
ssl_certificate_key /etc/ssl/privkey.pem;
# 500 MB covers backup-restore uploads (capped at 500 MB server-side).
client_max_body_size 500m;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
# Only needed if you use the MCP addon. Responses are Server-Sent Events,
# so buffering must be off or tool results arrive late.
location /mcp {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 3600s;
}
}
Caddy
trek.yourdomain.com {
reverse_proxy localhost:3000
}
Caddy handles TLS and WebSockets automatically.
Environment variables
Every variable, its default and what it does: see Environment Variables.
Star History
Data sources
The Atlas map's country and sub-national (province/county) boundaries come from geoBoundaries (Runfola et al., 2020), licensed CC BY 4.0. See NOTICE.md for full third-party attributions.
License
TREK is AGPL v3. Self-host freely for personal or internal company use. If you modify and offer TREK as a network service to third parties, your modifications must be open-sourced under the same licence.



