| .cargo | ||
| crates | ||
| docs | ||
| web | ||
| .containerignore | 27 B | |
| .gitignore | 52 B | |
| Cargo.lock | 93.4 KB | |
| Cargo.toml | 330 B | |
| compose.yaml | 448 B | |
| Containerfile | 844 B | |
| README.md | 6.3 KB |
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
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
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 cookieSecure. - 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 loginand 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. |