Every thread, woven together. A self-hostable location timeline + family portal: continuous location backup no phone swap can erase, plus a live family map with place-based alerts. No central service, no third party in the loop.
See PLAN.md for the full design.
- OwnTracks (iOS/Android, HTTP mode) POSTs to
POST /pub— authenticated per device with a token minted in the portal (QR provisioning). - ingest (Fastify) validates and INSERTs points. Its DB role cannot read
location history — popping
/publeaks nothing. - portal (Fastify + React/MapLibre PWA) is Weft's own auth (Argon2id + server-side sessions), live map over WebSocket, member-level timelines, admin, device minting, place alerts + locate-ping via Web Push (VAPID).
- Ingest→portal handoff is Postgres
NOTIFY: the portal computes place enter/exit (against each place's radius) and fans live positions out to visible watchers. - Caddy routes one public hostname:
/pub→ ingest, everything else → portal. - Postgres 16 + PostGIS is the single source of truth. History is
append-only; devices are retired, never deleted (
ON DELETE RESTRICT).
cp .env.example .env # edit: PUBLIC_URL, passwords, ADMIN_EMAIL
docker compose up -d --build
docker compose logs portal # first run prints the one-time admin passwordPoint your tunnel (e.g. Cloudflare Tunnel) at http://<host>:8080 for your
PUBLIC_URL hostname. Then:
- Log in at
PUBLIC_URL, change the admin password (Profile → password). - Add each family member — either directly (Admin tab; child accounts are admin-created) or by sending them an invite link (Invites tab, copyable or shareable via the OS share sheet) that lets them set their own password. Standard members can also self-signup at the login screen and await approval.
- On each phone: Devices → Add device → scan the QR with OwnTracks (install OwnTracks, set nothing up manually — the QR carries endpoint URL, username/device id, and the device token). Points flow immediately.
- Anyone can drop a Place (Places tab: click the map, name it, set a radius) — "Home," "School," etc. — for alert rules to reference.
- Install the portal to the home screen (PWA) and allow notifications to get place alerts + locate-ping (iOS 16.4+ requires home-screen install).
Requires Node 20+. Postgres+PostGIS via docker compose up -d db migrate.
npm install
npm run build # builds shared, db, ingest, portal, web
npm run typecheck
# run services locally (needs DATABASE_URL, PUBLIC_URL=http://localhost:5173 ...)
node apps/ingest/dist/index.js
node apps/portal/dist/index.js
npm run dev -w @weft/web # vite dev server, proxies /api and /pubMigrations are plain SQL in db/migrations/, applied in order by
packages/db (npm run migrate, superuser DSN) — the compose migrate
service does this automatically before ingest/portal start.
- Two DB roles:
weft_ingest(INSERT points, token lookup,last_seen_atonly) andweft_portal(app management, read-only history). /pubrejects unknown/retired tokens; there is no public device-creation path. Tokens and session ids are stored hashed (sha256), passwords Argon2id.- Sessions: httpOnly Secure SameSite=Strict cookies + custom-header CSRF check.
- Visibility ("weaves", incl. directional parent/child) is re-resolved at every read, WS fan-out, and alert delivery — not just at setup time.
- Hidden devices are backed up and locatable by their owner, but never appear on the shared map or in other members' alerts.
Implemented: phases 1–4 of the plan plus most of 5–6 (places — a named pin + adjustable radius, any member can create one — enter/exit detection, alert rules with a UI to create/edit/enable/delete them, Web Push, locate-ping, hidden devices, weave management UI, invite-based onboarding, self-service password reset). Not yet: Google Timeline backfill importer (phase 7), TOTP 2FA.
SMTP is entirely optional throughout, not a requirement: without it, invites show a copyable/shareable link instead of emailing one, and a "forgot password" request queues for an admin to action (Admin tab, generates a one-time password) instead of emailing a reset link.
Note: what PLAN.md §7 calls "geofence" (an admin-drawn polygon) shipped
instead as "place" — a member-creatable point + radius, checked by distance
(ST_DWithin) rather than polygon containment. Simpler to draw, simpler to
reason about, and covers the real use case ("alert when Seth leaves school").