- Dart 59.6%
- Rust 26.9%
- Nix 3.9%
- CMake 3%
- Kotlin 2.7%
- Other 3.9%
| .forgejo/workflows | ||
| app | ||
| backend | ||
| .envrc | ||
| .gitignore | ||
| .woodpecker.yml | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| rust-toolchain.toml | ||
Famtrack
A libre, self-hostable family location tracker — modern alternative to OwnTracks.
- Backend: Rust + Axum + sqlx + PostgreSQL. At-rest encryption for locations, geofences, dead-zones, and battery samples (ChaCha20-Poly1305 with a server-held master key).
- App: Flutter (Android-first; flake also supports Linux desktop). OpenStreetMap via
flutter_map. UnifiedPush for push notifications — no Google services. - Notifications: server-side geofence enter/exit + battery-threshold evaluation. Push payloads carry meaningful summaries ("Alice arrived at Home").
Repo layout
backend/ Rust server + Postgres migrations + NixOS module + sqlx offline data
app/ Flutter app (Android + Linux)
flake.nix Dev shells + package + NixOS module
Prerequisites
- Nix with flakes enabled. Everything below assumes you're inside one of the dev shells.
- For the Android app: ~6 GB free for Gradle + the Android SDK.
Backend — development
nix develop # default shell: Rust, Postgres, sqlx-cli, Flutter
1. Start a local Postgres
The unix socket dir must live outside the repo (the flake source filter rejects socket files):
initdb -D .pg --no-locale --encoding=UTF8 -U "$USER"
pg_ctl -D .pg -l .pg.log -o "-p 55432 -h 127.0.0.1 -k /tmp/famtrack-pgsock" start
createdb -h 127.0.0.1 -p 55432 famtrack
2. Write the dev .env
cp backend/.env.example backend/.env
# generate the at-rest encryption key (32 bytes hex)
sed -i "s/^DATA_KEY_HEX=.*/DATA_KEY_HEX=$(openssl rand -hex 32)/" backend/.env
sed -i "s/^DATABASE_URL=.*|DATABASE_URL=postgres://$USER@127.0.0.1:55432/famtrack|" backend/.env
3. Build & run
cd backend
export $(grep -v ^# .env | xargs)
cargo run # migrations apply on first boot
backend/scripts/smoke.sh exercises the full API (register → invite → join → encrypted geofence → location post → events → dead-zone). With the server running:
BASE=http://127.0.0.1:8080 ./backend/scripts/smoke.sh
4. After schema changes
sqlx::query! is compile-time checked. After editing a migration:
sqlx migrate run --database-url "$DATABASE_URL"
cargo sqlx prepare # refresh backend/.sqlx/ — commit it
The Nix package build relies on .sqlx/ (offline mode), so do not skip the second step.
App — development
nix develop .#android # extra shell with Android SDK, NDK, JDK 21
cd app
# First-time only: generate android/, linux/ platform scaffolding.
flutter create . --project-name=famtrack --org=xyz.mrfluffy --platforms=android,linux
flutter pub get
flutter analyze
flutter run # picks an attached device / emulator
flutter build apk --debug # → build/app/outputs/flutter-apk/app-debug.apk
The Android emulator reaches a host-side server on http://10.0.2.2:8080. For a physical phone, set the server URL in the app's Settings screen.
App ↔ server reachability
| Setup | Server BIND_ADDR |
App baseUrl |
|---|---|---|
| Android emulator on dev host | 127.0.0.1:8080 |
http://10.0.2.2:8080 (default) |
| Phone on LAN | 0.0.0.0:8080 |
http://<host-lan-ip>:8080 (set in Settings) |
| Production | 127.0.0.1:8080 behind nginx/caddy |
https://famtrack.example.com (set in Settings) |
Server — deployment on NixOS (flake module)
The flake exports a NixOS module. In your host flake:
{
inputs.famtrack.url = "github:yourorg/famtrack"; # or path:/srv/famtrack
outputs = { self, nixpkgs, famtrack, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
famtrack.nixosModules.default
({ ... }: {
services.famtrack-server = {
enable = true;
bindAddr = "127.0.0.1:8080";
environmentFile = "/var/lib/secrets/famtrack.env";
# provisionDatabase = true; # default — also enables postgresql.service
# openFirewall = false; # default — front with nginx/caddy
};
})
];
};
};
}
Provision the secrets file
environmentFile must hold:
JWT_SECRET=<openssl rand -hex 32>
DATA_KEY_HEX=<openssl rand -hex 32>
Permissions:
install -d -m 0700 -o root -g root /var/lib/secrets
install -m 0600 -o root -g root /dev/stdin /var/lib/secrets/famtrack.env <<EOF
JWT_SECRET=$(openssl rand -hex 32)
DATA_KEY_HEX=$(openssl rand -hex 32)
EOF
For production: use sops-nix or agenix to keep these out of the Nix store and your git history.
⚠️ The data key is load-bearing. Losing
DATA_KEY_HEXpermanently bricks every encrypted row in the DB. Back it up offline.
Deploy
sudo nixos-rebuild switch --flake .#my-host
sudo systemctl status famtrack-server
sudo journalctl -u famtrack-server -f
The module:
- creates a
famtracksystem user + group, - provisions a local Postgres +
famtrackrole/database (toggleable), - runs
famtrack-serverwith strict systemd sandboxing, - waits on
postgresql.servicebefore starting.
Reverse proxy (nginx example)
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
virtualHosts."famtrack.example.com" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
proxyWebsockets = true; # required — clients hold WebSockets at /v1/ws/...
};
};
};
security.acme.acceptTerms = true;
security.acme.defaults.email = "you@example.com";
Just the package (no module)
nix build github:yourorg/famtrack#famtrack-server
./result/bin/famtrack-server # needs DATABASE_URL, JWT_SECRET, DATA_KEY_HEX
UnifiedPush
The app uses UnifiedPush. To get push notifications working end-to-end:
- Install a distributor app on the phone (e.g. ntfy, NextPush).
- Open Famtrack — it picks up the distributor automatically, requests an endpoint, and registers it against the server (
POST /v1/devices). - The server posts a small JSON ping (
{"type":"famtrack.event","summary":"…"}) to that endpoint when an event fires.
Without a distributor, the app still works while open — events arrive over the WebSocket.
Troubleshooting
cargo runsaysDATA_KEY_HEX not set—export $(grep -v ^# backend/.env | xargs)before running, orsystemctl restart famtrack-serverif you just changed the env file.nix developfails withunsupported typeon.s.PGSQL.*— Postgres' unix socket landed in the repo. Always runpg_ctlwith-k /tmp/famtrack-pgsock(or any path outside the flake), thenrm -f .s.PGSQL.*.flutter build apk"Duplicate class … tink" — already fixed inapp/android/app/build.gradle.kts; if it returns, look for another transitive depending oncom.google.crypto.tink:tink(the JVM build) and add the exclusion.flutter build apk"Language version 1.7 is no longer supported" — an Android plugin pinned an old Kotlin language version. Bump that plugin (we already did this forunifiedpush 5 → 6).- Server-side build fails with sqlx "DATABASE_URL not set" — for
cargo buildsetDATABASE_URL. Fornix buildwe useSQLX_OFFLINE=true; you must have runcargo sqlx preparesince the last migration change and committedbackend/.sqlx/.
License
AGPL-3.0-only.