⎇
.cargo
android
crates
docs
web
.containerignore27 B
.gitignore52 B
Cargo.lock93.4 KB
Cargo.toml330 B
compose.yaml448 B
Containerfile844 B
README.md6.7 KB
READMERaw

opentracker

Self-hosted location sharing. Devices upload positions over HTTPS. A web map shows your position and the positions others share with you.

crates/api     JSON types shared by server, CLI and web
crates/server  otserver: axum + SQLite, serves the API and web/dist
crates/cli     ot: test client (register a device, send points, simulate a walk)
web/           Leptos + Leaflet, built with Trunk
android/       Android app: background tracking, this device's map, see docs/android.md

Development

cd web && trunk build && cd ..     # or `trunk serve`: live reload on :8081, API proxied to :8080
cargo run -p server                # http://localhost:8080, the first visit creates the admin account

cargo run -p cli -- login http://localhost:8080 alice
cargo run -p cli -- simulate --interval 2 --batch 5 48.137 11.575
cargo run -p cli -- people

Android app

Needs JDK 17 or newer and the Android SDK in ANDROID_HOME:

cd android
ANDROID_HOME=~/android-sdk ./gradlew testDebugUnitTest assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Debug builds allow plain HTTP. In the emulator, the server on the host is http://10.0.2.2:8080. adb emu geo fix <lon> <lat> moves the emulator's GPS.

Configuration

Every flag can also be set by its environment variable. otserver --help lists them.

Flag Variable Default
--addr OT_ADDR 127.0.0.1:8080 listen address
--db OT_DB ot.db SQLite file
--web-dir OT_WEB_DIR web/dist built web UI
--public-url OT_PUBLIC_URL the address browsers use, see below
--retention-days OT_RETENTION_DAYS 30 days to keep points, 0 keeps them forever. Users can choose a shorter time. Each device keeps its newest point, so it stays on the map.

--public-url matters behind a reverse proxy:

  • Passkeys are bound to this address. Without it the server uses the request's Host header and assumes plain HTTP.
  • An https:// URL marks the session cookie Secure.
  • The web UI shows it in the device setup commands.

The server updates the database schema at start. Back up the database file before you upgrade.

otserver passwd <user> creates a user or resets a password. A reset also removes all passkeys of the user and turns off two-factor sign-in, so a lost device cannot sign in.

Accounts and sign-in

  • The first visit to a server with no users shows a setup form for the admin account. Anyone who reaches the server first can claim it, so set it up before you expose it.
  • Admins add and delete users, change their role and reset passwords under Settings → Users. Admins cannot change their own role, so one admin always remains.
  • Each user picks a sign-in mode under Settings → Security: password or passkey, or password and passkey (two-factor). A user with a passkey can remove the password.
  • Devices sign in with a token. Create one under Settings → Devices, or register with ot login and the password. Two-factor accounts must use a token.

Devices and sharing

  • A person can upload from several devices at once. Each device keeps its own trail. The map shows one dot per person, at the newest position of any device.
  • The web app counts as one device, "Web", in every browser.
  • Removing a device deletes its points.
  • A share chooses what the viewer sees:
    • all devices, including ones added later, or only selected devices,
    • only the current position, or the trail from now on, since a chosen time, or the full history,
    • the exact position, or one rounded to about 100 m, 1 km or 10 km.
  • Sharing again with the same person replaces the settings.
  • A guest link shares with anyone who has the link, without an account. It has the same choices, plus an optional password. The link has the form https://track.example.com/#l=<token>. The token stays after the #, so it does not reach server or proxy logs when the page loads.

Deployment

OT_PUBLIC_URL=https://track.example.com docker compose up -d

The container listens on 127.0.0.1:8080. Put a TLS reverse proxy in front of it. A proxy with HTTP/3 is recommended: phones connect faster after sleeping, and the connection survives network changes. Caddy does HTTP/3 by default:

track.example.com {
	reverse_proxy 127.0.0.1:8080
}

HTTP/3 needs UDP 443 open next to TCP 443. ot --http3 people prints [HTTP/3.0] when it works.

API

Web endpoints use the session cookie. Devices use Authorization: Bearer <token>.

GET/POST /api/setup none first-boot admin account
POST /api/login, /api/logout password can answer with a passkey challenge (two-factor)
POST /api/passkey/login[/finish] none passkey sign-in. Can ask for the password next (two-factor)
GET /api/me session
POST/DELETE /api/me/password, PUT /api/me/two-factor, PUT /api/me/retention session
GET /api/passkeys, POST /api/passkeys/register[/finish], DELETE /api/passkeys/{id} session
GET /healthz none ok while the database answers
GET /api/people session you plus everyone who shares with you, with each visible device and its latest point
GET /api/people/{id}/track?from=&to=&device= session one device's points in a time range, at most 31 days
GET/POST /api/devices, DELETE /api/devices/{id} session POST creates a device token
GET/POST /api/shares, DELETE /api/shares/{id} session POST with an existing viewer replaces that share
GET /api/usernames session all other usernames, for the share form
GET/POST /api/links, DELETE /api/links/{id} session guest links
POST /api/guest, /api/guest/track link token (+ key) what a guest link shows. 401 means the link needs its password
POST /api/guest/unlock link token + password returns the key for a password-protected link. 5 failures lock the link for 15 minutes
GET/POST /api/users, DELETE /api/users/{id}, PUT /api/users/{id}/role, POST /api/users/{id}/password admin
POST /api/devices/register username + password returns a device token
POST /api/devices/pair/begin session {challenge, name} → one-time code for the app, valid 5 minutes
POST /api/devices/pair code + verifier the app exchanges the code and the secret behind the challenge for a device token. Each code works once.
GET /api/device, GET /api/device/track?from=&to= device token the app's own position and trail. The web UI's #device page uses them inside the app.
POST /api/points device token or session JSON array of points, at most 1000. Returns the visible people. Duplicates (same device and second) are ignored, so a client can retry a batch. A session uploads as the "Web" device.