* fix(security): equalise login response timing to prevent user enumeration (CWE-208)
Always run bcrypt.compareSync regardless of whether the email exists, using a
module-scope DUMMY_PASSWORD_HASH for unknown/OIDC-only accounts. Also wraps the
login handler in a 350ms minimum-latency pad (matching /forgot-password) as
defence-in-depth against CPU jitter and future code-path drift.
Fixes: CWE-203, CWE-208 — Observable Timing Discrepancy (CVSS 5.3 Medium)
* chore(deps): patch hono/picomatch/ip-address/brace-expansion CVEs, bump to node:24-alpine
Extends server/package.json overrides to pin hono >=4.12.16, picomatch >=4.0.4,
brace-expansion >=2.0.3, ip-address >=10.1.1. Adds matching overrides to client/.
Lockfiles regenerated to resolve: hono 4.12.18, ip-address 10.2.0, picomatch 4.0.4.
Also bumps base image node:22-alpine -> node:24-alpine (reduces base image CVEs)
and adds .github/workflows/security.yml to gate PRs on critical/high CVEs via
Docker Scout.
Addresses: CVE-2026-44456, CVE-2026-44455 (hono), CVE-2026-42338 (ip-address),
CVE-2026-33671, CVE-2026-33672 (picomatch), CVE-2026-33750 (brace-expansion)
* chore: update emails in security.md
* ci(security): use docker/login-action for Scout auth instead of env vars
* chore: regenerate lock files
* chore: correct secret names
* chore: pr perms write
* fix(docker): remove package-lock.json from production image after npm ci
Docker Scout reads package-lock.json as an SBOM source and reports all
lockfile entries including devDependencies (e.g. picomatch via vitest/vite)
even when they are not physically installed. The lockfile has no runtime
purpose after npm ci completes, so delete it to ensure Scout only reports
packages actually present in node_modules.
* fix(docker): remove npm CLI from production image to eliminate bundled CVEs
picomatch@4.0.3, brace-expansion@5.0.4, and ip-address@10.1.0 were all
coming from /usr/local/lib/node_modules/npm — npm's own bundled packages
shipped with node:24-alpine. The production container only needs the node
binary to run the server; npm is unused at runtime.
Removing npm + npx after npm ci drops the package count from 500 to 365
and eliminates all npm-ecosystem CVEs (0H 0M remaining from npm packages).
Only busybox CVE-2025-60876 remains, which has no fix in Alpine 3.23.
* fix(deps): remove client overrides and brace-expansion server override; audit fix
brace-expansion ^2.0.3 in the client forced all installations to v2, breaking
minimatch in CI (test:coverage path via @vitest/coverage-v8 -> test-exclude)
which expects the named-export API of brace-expansion v5. The CVE it targeted
(>=4.0.0,<5.0.5) was only in npm's own bundled packages, already eliminated
by removing npm from the Docker image.
Also removes picomatch and ip-address client overrides for the same reason:
all three CVEs sourced from /usr/local/lib/node_modules/npm/, not app deps.
Drops brace-expansion from server overrides (server uses v2.1.0, outside the
affected range >=4.0.0).
* fix(#981): align public share itinerary order with daily planner (#985)
The public share page rendered daily items in a different order than the
authenticated planner because it used a simplified, divergent merge
algorithm. Five specific bugs:
1. shareService never loaded reservation_day_positions, so per-day
transport positions were lost on the share page (fell back to
day_plan_position ?? 999, pushing transports to the bottom).
2. Multi-day transports (overnight trains/flights) only appeared on their
start day due to date-string filtering instead of day_id span logic.
3. Assignment-linked transports appeared twice (once as place, once as
transport card) because the assignment_id exclusion was missing.
4. Time-based transport insertion was absent; missing positions used 999
instead of a computed fractional position from the place timeline.
5. created_at tiebreaker was missing for assignments and notes with equal
order_index/sort_order, making order non-deterministic on the share page.
Fix: extract the authoritative merge logic (parseTimeToMinutes,
getSpanPhase, getDisplayTimeForDay, getTransportForDay, getMergedItems)
from DayPlanSidebar into client/src/utils/dayMerge.ts and use it in both
the planner and SharedTripPage. Enrich the shareService payload with
day_positions from reservation_day_positions and add created_at tiebreakers
to the assignment and day_notes ORDER BY clauses.
* fix(#983): shift owner vacay entries when update_trip moves trip window
updateTrip() now calls shiftOwnerEntriesForTripWindow() which looks up
the owner's own vacay plan (not the active plan) and shifts all entries
in the old date window by the same offset as the trip start date.
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
🧭 Trip planning
|
🧳 Travel management
|
👥 Collaboration
|
📱 Mobile & PWA
|
🧩 Addons (admin-toggleable)
|
🤖 AI / MCP
|
⚙️ Admin & customisation
|
|
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). State with Zustand. Auth via JWT + OAuth 2.1 + OIDC + TOTP MFA. Weather via Open-Meteo (no key required). Maps with Leaflet and Mapbox GL.
Docker Compose (production)
Full compose example with secure defaults
services:
app:
image: mauriceboe/trek:latest
container_name: trek
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
tmpfs:
- /tmp:noexec,nosuid,size=64m
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-} # generate with: openssl rand -hex 32
- TZ=${TZ:-UTC}
- LOG_LEVEL=${LOG_LEVEL:-info}
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-}
- APP_URL=${APP_URL:-} # required for OIDC + email links
# - FORCE_HTTPS=true # behind a TLS-terminating proxy
# - TRUST_PROXY=1
# - OIDC_ISSUER=https://auth.example.com
# - OIDC_CLIENT_ID=trek
# - OIDC_CLIENT_SECRET=supersecret
# - OIDC_DISPLAY_NAME=SSO
# - OIDC_ADMIN_CLAIM=groups
# - OIDC_ADMIN_VALUE=app-trek-admins
volumes:
- ./data:/app/data
- ./uploads:/app/uploads
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
Then:
docker compose up -d
HTTPS notes: FORCE_HTTPS=true is optional — it adds a 301 redirect, HSTS, CSP upgrade-insecure-requests, and forces the secure cookie flag. Only use it behind a TLS-terminating reverse proxy. TRUST_PROXY=1 tells Express how many proxies sit in front so real client IPs and X-Forwarded-Proto work.
Helm (Kubernetes)
helm repo add trek https://mauriceboe.github.io/TREK
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
Docker Compose:
docker compose pull && docker compose up -d
Docker run — reuse the original volume paths:
docker pull mauriceboe/trek
docker rm -f trek
docker run -d --name trek -p 3000:3000 -v ./data:/app/data -v ./uploads:/app/uploads --restart unless-stopped mauriceboe/trek
Not sure which paths you used?
docker inspect trek --format '{{json .Mounts}}'before removing the container.
Your data stays in the mounted data and uploads volumes — updates never touch it.
Rotating the Encryption Key
If you need to rotate ENCRYPTION_KEY (e.g. upgrading from a version that derived encryption from JWT_SECRET):
docker exec -it trek node --import tsx scripts/migrate-encryption.ts
The script creates a timestamped DB backup before making changes and prompts for old + new keys (input is not echoed).
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.
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;
}
}
Caddy
trek.yourdomain.com {
reverse_proxy localhost:3000
}
Caddy handles TLS and WebSockets automatically.
Environment variables
Full reference
| Variable | Description | Default |
|---|---|---|
| Core | ||
PORT |
Server port | 3000 |
NODE_ENV |
Environment (production / development) |
production |
ENCRYPTION_KEY |
At-rest encryption key for stored secrets (API keys, MFA, SMTP, OIDC). Recommended: generate with openssl rand -hex 32. If unset, falls back to data/.jwt_secret (existing installs) or auto-generates a key (fresh installs). |
Auto |
TZ |
Timezone for logs, reminders and cron jobs (e.g. Europe/Berlin) |
UTC |
LOG_LEVEL |
info = concise user actions, debug = verbose details |
info |
DEFAULT_LANGUAGE |
Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar |
en |
ALLOWED_ORIGINS |
Comma-separated origins for CORS and email links | same-origin |
FORCE_HTTPS |
Optional. When true: 301-redirects HTTP to HTTPS, sends HSTS, adds CSP upgrade-insecure-requests, forces the session cookie secure flag. Useful behind a TLS-terminating reverse proxy. Requires TRUST_PROXY. |
false |
HSTS_INCLUDE_SUBDOMAINS |
When true: adds the includeSubDomains directive to the HSTS header, extending HTTPS enforcement to all subdomains. Only effective when HSTS is active (FORCE_HTTPS=true or NODE_ENV=production). Leave false if you run other services on sibling subdomains over plain HTTP. |
false |
COOKIE_SECURE |
Controls the secure flag on the trek_session cookie. Auto-derived: on when NODE_ENV=production or FORCE_HTTPS=true. Escape hatch: set false to allow session cookies over plain HTTP. Not recommended in production. |
auto |
TRUST_PROXY |
Number of trusted reverse proxies. Tells Express to read client IP from X-Forwarded-For and protocol from X-Forwarded-Proto. Defaults to 1 in production; off in dev unless set. |
1 |
ALLOW_INTERNAL_NETWORK |
Allow outbound requests to private/RFC-1918 IPs (e.g. Immich on your LAN). Loopback and link-local addresses remain blocked. | false |
APP_URL |
Public base URL of this instance (e.g. https://trek.example.com). Required when OIDC is enabled; used as base for email notification links. |
— |
| OIDC / SSO | ||
OIDC_ISSUER |
OpenID Connect provider URL | — |
OIDC_CLIENT_ID |
OIDC client ID | — |
OIDC_CLIENT_SECRET |
OIDC client secret | — |
OIDC_DISPLAY_NAME |
Label shown on the SSO login button | SSO |
OIDC_ONLY |
Force SSO-only mode: disables password login + registration, regardless of Admin > Settings. The first SSO login becomes admin. | false |
OIDC_ADMIN_CLAIM |
OIDC claim used to identify admin users | — |
OIDC_ADMIN_VALUE |
Value of the OIDC claim that grants admin role | — |
OIDC_SCOPE |
Space-separated OIDC scopes. Fully replaces the default — always include openid email profile. |
openid email profile |
OIDC_DISCOVERY_URL |
Override the auto-constructed OIDC discovery endpoint (e.g. Authentik: .../application/o/trek/.well-known/openid-configuration) |
— |
| Initial setup | ||
ADMIN_EMAIL |
Email for the first admin on initial boot. Must be set together with ADMIN_PASSWORD. If either is omitted a random password is printed to the server log. No effect once a user exists. |
admin@trek.local |
ADMIN_PASSWORD |
Password for the first admin on initial boot. Pairs with ADMIN_EMAIL. |
random |
| Other | ||
DEMO_MODE |
Enable demo mode (hourly data resets) | false |
MCP_RATE_LIMIT |
Max MCP API requests per user per minute | 300 |
MCP_MAX_SESSION_PER_USER |
Max concurrent MCP sessions per user | 20 |
Data & Backups
- Database — SQLite, stored in
./data/travel.db - Uploads — stored in
./uploads/ - Logs —
./data/logs/trek.log(auto-rotated) - Backups — create and restore via Admin Panel
- Auto-Backups — configurable schedule and retention in Admin Panel
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.








