- GET /api/directions proxies OSRM (OSRM_URL env, default public demo server) with validation, Leaflet-order geometry, 24h capped cache - Map draws straight lines first, then upgrades ground legs to road-following polylines with routed km (marked road) as directions arrive; silent fallback to great-circle on failure; air legs unchanged - README documents OSRM_URL
5.5 KiB
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-FRAand 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_SECRETindocker-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
- docs/PROJECT_OVERVIEW.md — architecture, data model, and design decisions.
- docs/API.md — the REST API contract.