React/Vite frontend and Express/SQLite backend for tracking visited and planned cities on a map, with per-city notes, photos, links, and participant-based sharing between family members. Includes session auth with admin-managed user accounts, an OSM/Nominatim geocoding proxy, and a single-image Docker deployment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| client | ||
| server | ||
| shared | ||
| .dockerignore | ||
| .env.example | ||
| .eslintrc.json | ||
| .gitignore | ||
| .prettierrc.json | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
CityTracker
A self-hostable webapp for tracking cities you've visited (or plan to visit) on a map, with ratings, notes, photos, and links — built for sharing between family members or a partner, each with their own login.
Features
- Interactive world map (Leaflet + OpenStreetMap tiles, no API keys) with pins colored by status (visited / planned) and a heart badge for favorites
- Add a city by searching its name (via OpenStreetMap Nominatim) or by clicking directly on the map
- Per-city notes, a "liked" heart, one photo, a list of links, and the people who were there (picked from your user list)
- Filter the map by visited/planned, favorites, year, or country
- Multiple user accounts; a city is visible/editable by everyone listed as a participant on it
- Admin-managed user accounts (no public self-registration)
- Single Docker image, SQLite database, file-based photo storage — easy to back up as one Docker volume
Tech stack
- Frontend: React + TypeScript, Vite, React Router, TanStack Query, Leaflet
- Backend: Node.js + TypeScript, Express, better-sqlite3, argon2, multer
- Database: SQLite, single file, plain SQL migrations run automatically on startup
- Auth: server-side sessions (httpOnly cookies), backed by a SQLite table so logins survive container restarts
Project structure
CityTracker/
├── shared/ # TypeScript types + zod validation schemas, used by both client and server
├── server/ # Express API (auth, cities, uploads, geocoding proxy)
└── client/ # React app (Vite)
Running with Docker (recommended)
This is the intended way to run CityTracker — one image, one volume.
-
Copy the example environment file and fill in real values:
cp .env.example .envAt minimum, set:
SESSION_SECRET— a long random string (generate one withnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))")ADMIN_USERNAME/ADMIN_PASSWORD— credentials for the first admin account, created automatically the first time the server starts. If you leave these unset, open the app once it's running and use the one-time/setupscreen instead.
-
Build and start the container:
docker compose up -d --build -
Open
http://<your-host>:<HOST_PORT>(default port3000) and log in with the admin account you configured.
All application data — the SQLite database and uploaded photos — lives under
/app/data inside the container, on the citytracker-data named volume. Back
up that volume (e.g. docker run --rm -v citytracker_citytracker-data:/data -v $(pwd):/backup alpine tar czf /backup/citytracker-backup.tar.gz /data)
to back up everything.
Behind Nginx Proxy Manager (or another reverse proxy)
The container just needs to be reachable and listening on HOST_PORT. Point a
proxy host at docker.covenant.lan:<HOST_PORT> (or wherever the container
runs), enable SSL as usual in your proxy. If your proxy terminates TLS and
forwards plain HTTP to the container, set COOKIE_SECURE=true in .env so
session cookies are still marked Secure correctly; leave it false if the
container itself is only ever reached over plain HTTP inside your LAN.
First admin account
On first startup, if no admin user exists yet:
- If
ADMIN_USERNAMEandADMIN_PASSWORDare set in.env, that account is created automatically. - Otherwise, visiting the app shows a one-time setup screen to create the first admin account by hand.
After that, only an admin can create further user accounts, from the Users page in the app.
Adding more users
Log in as an admin, go to Users, and use the "Add a user" form. Admins can also promote/demote other admins, deactivate/reactivate accounts, and reset passwords from the same page. Deactivated accounts can no longer log in but remain visible (grayed out) on cities they previously participated in, so history isn't lost.
Local development (without Docker)
Requires Node.js 20 (native modules better-sqlite3 and argon2 need a
recent Node build with prebuilt binaries available).
npm install
npm run build -w shared # shared types must be built once before starting the server or client
# in one terminal
PORT=3000 SESSION_SECRET=dev-secret ADMIN_USERNAME=admin ADMIN_PASSWORD=devpassword123 npm run dev:server
# in another terminal
npm run dev:client
The Vite dev server (http://localhost:5173) proxies /api and /uploads
requests to the backend on port 3000.
To produce a production build of everything (used by the Docker image):
npm run build
npm start
Configuration reference
All configuration is via environment variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
PORT |
Port the server listens on inside the container | 3000 |
HOST_PORT |
Port exposed on the Docker host (docker-compose only) | 3000 |
SESSION_SECRET |
Secret used to sign session cookies. Required in production. | — |
ADMIN_USERNAME / ADMIN_PASSWORD |
Credentials for the auto-created first admin account | — |
DB_PATH |
Path to the SQLite database file | /app/data/citytracker.db (Docker) |
UPLOAD_DIR |
Directory where uploaded photos are stored | /app/data/uploads (Docker) |
COOKIE_SECURE |
Mark session cookies Secure (only if TLS reaches the container or is terminated just in front of it) |
false |
Notes on scope / what's not included
- Trips (grouping cities into a named trip with its own date range) is
planned as a future addition. The database schema was deliberately kept
simple enough that adding
trips/trip_citiestables later won't require reworking existing data. - The UI is English-only by design, to keep the app simple; phone/browser auto-translate tools work fine over it if needed.
- Uploaded photos are served from
/uploads/<random-filename>without a per-request auth check — acceptable for a private LAN/home-use deployment since filenames are non-guessable UUIDs, but worth knowing if you expose this instance beyond your own network. - There is no automated test suite; this is a personal/family-scale project verified through manual end-to-end testing.