grabowski 9369c82e56 Give each map leg its own colour
Legs cycle through the shared 8-hue palette by index: polylines (air
stays dashed), km-label accents, and the Legs-list number badges all
match, so multiple car routes are distinguishable. Badges use dark text
after a contrast check ruled out white on these hues.
2026-07-20 11:03:11 +07:00
2026-07-20 11:03:11 +07:00

Trip Plan

A self-hosted, multi-user web tool for collaboratively planning trips. Multiple people log in to the same instance, create trips with a name and date range, and fill in a day-by-day calendar together — flights (with multi-leg connections), hotels, border crossings, drives, rental cars, and activities. Each trip is drawn on an interactive map with the route between stops, per-leg distances, cost splitting between members, and an overall summary. It runs as a single Docker container with a SQLite database.

Features

  • Accounts without passwords — Mullvad-style: one click generates a random account number (shown once, stored only as a hash). No email, no password, no personal data.
  • Sharing via join codes — every trip has a short code; anyone with an account enters it to become a co-editor. The owner can regenerate the code to stop further joins.
  • Day-by-day calendar — a real calendar grid; click a day to add any number of entries (activity, hotel, travel, flight, rental car, immigration, note).
  • Multi-leg flights — type CNX-BKK-DXB-FRA and it expands into segments with per-leg flight number and times; every airport becomes a stop on the map (bundled offline IATA airport database).
  • Rental cars — brand/model/type, booking ref, pickup/dropoff with dates and times, and included km — compared against the trip's estimated driven km.
  • Locations — geocoded via OpenStreetMap (Nominatim, English results); lat/lng stored per entry.
  • Map — Leaflet + OpenStreetMap; stops plotted in order, air legs dashed / ground legs solid, per-leg great-circle km.
  • Costs & splitting — price per entry with "split equally", "everyone pays their own", or "payer's expense"; per-person share/paid/net table and minimal settle-up transfers, in the trip's currency.
  • Summary — days/nights, entry counts, total km, ≈ km driven (rental-car figure) vs km flown, and locations visited.

Deployment (Docker + external Caddy for TLS)

The app serves plain HTTP on port 3000 and is designed to sit behind a reverse proxy that terminates TLS — e.g. an external Caddy.

1. Run the container

Using the prebuilt image from the registry:

# docker-compose.yml
services:
  tripplan:
    image: git.b4l.co.th/grabowski/trip-plan:latest
    restart: unless-stopped
    environment:
      SESSION_SECRET: "<long random string — e.g. openssl rand -hex 32>"
    volumes:
      - ./data:/app/data      # database lives next to this compose file
    # No public port mapping needed when Caddy shares a Docker network with
    # the app (recommended). For a host-level Caddy, map localhost only:
    ports:
      - "127.0.0.1:3000:3000"

The entrypoint chowns ./data to the app's unprivileged node user on startup, so a root-created bind-mount directory works out of the box. (Images older than this note need a one-time sudo chown -R 1000:1000 ./data.)

docker compose up -d

(Or build locally instead of pulling: clone this repo and use build: . — the checked-in docker-compose.yml does exactly that.)

2. Point Caddy at it

If Caddy runs on the same host (host-level Caddy, app bound to 127.0.0.1:3000):

trips.example.com {
    reverse_proxy 127.0.0.1:3000
}

If Caddy runs as a container, attach both services to a shared Docker network and proxy by service name instead:

trips.example.com {
    reverse_proxy tripplan:3000
}

That's all — Caddy obtains and renews the certificate automatically; the app needs no TLS configuration of its own. Session cookies are SameSite=Lax and httpOnly, and work as-is behind the proxy.

3. Updating

docker compose pull && docker compose up -d

Schema migrations run automatically at startup; existing data in ./data is preserved (back up that directory to back up all trips).

Quick start (local, no proxy)

docker compose up -d      # builds the image from source
# open http://localhost:3000 and create the first account

Before exposing the app beyond localhost, set your own random SESSION_SECRET in docker-compose.yml.

Local development

npm install
npm start        # http://localhost:3000
npm test         # API tests (node:test + supertest)

npm run dev starts the server with --watch for auto-reload.

Environment variables

Variable Default Description
PORT 3000 HTTP port the server listens on.
DATA_DIR ./data (/app/data in Docker) Directory holding the SQLite database file.
SESSION_SECRET dev value (warns) Secret used to sign session cookies. Set this in production.
OSRM_URL https://router.project-osrm.org Base URL for the driving-directions proxy (/api/directions). The default is the public OSRM demo server — fine for light personal use. Self-hosters can point this at their own OSRM instance.

Data

All state lives in a single SQLite file under DATA_DIR. In Docker this is /app/data, bind-mounted from ./data next to the compose file — back up that directory to back up all trips. No external database or services are required (geocoding calls OpenStreetMap's Nominatim, which needs no API key; airport lookups use a bundled offline dataset; road directions call the public OSRM demo server, also no API key).

Documentation

S
Description
No description provided
Readme
725 KiB
Languages
JavaScript 88.9%
CSS 10.4%
Dockerfile 0.3%
HTML 0.3%
Shell 0.1%