Skip to content

Repository files navigation

starling

Private location sharing for friends and family. End to end encrypted, no accounts, no phone numbers, and a relay that stores nothing it could ever read.

CI

Live: starlingmap.app · grab the Android app there.

This document describes protocol v2: forward secrecy, post-compromise security, and cryptographic member removal. As of 0.5.0 it is wired end to end, crypto core through relay through storage through UI, on both web and Android, and 500-plus unit tests plus five end to end suites exercise it as a running app talking to a running relay. Nobody outside this project has independently reviewed any of it. See docs/AUDIT.md for exactly what has and has not been checked, including whether the live relay has actually been redeployed to speak v2 yet.

Life360 works by shipping everyone's location to a company. Starling keeps the Life360 features people actually want (live map of your people, SOS, check-ins, battery, invite links) and drops the surveillance: positions are encrypted on your device with a key the server never sees, and the relay holds at most 24 hours of ciphertext.

Circles are plural. Keep one for family and one for the friends you split up from at a fair or a concert, and switch between them with a tap on the circle name. Each circle has its own secret, its own channel, and its own signing identity, so the relay cannot tell that two circles share a member. The map, sharing, and invites all follow whichever circle is active.

The map with your circle on it

How it works

  • Creating a circle generates its keys on your device. Inviting someone shares a one-time invite secret, in the URL fragment, which browsers never send to any server; it is not the circle's own key material, so a stolen invite link is only good for one join attempt, and only if you accept a safety number you were not expecting. Share the link over something you already trust, like Signal, the same as always.
  • Locations are AES-256-GCM encrypted under a key that advances every 10 minutes (HKDF-SHA-256, one hash step forward per epoch) and is destroyed the moment a device moves past it. A device seized after it has advanced cannot decrypt what it already forgot, and neither can anyone who takes it. Names, avatars, and statuses ride inside the ciphertext too. Every plaintext is padded to exactly 512 bytes so message sizes carry nothing.
  • Each device signs its posts with its own Ed25519 key (P-256 fallback), and every receiving device checks that signature itself. The content key advances per member as well as per epoch, so it proves only that a message came from a key material holder; the signature is what says which member. The relay pins each key on first write, so nobody can overwrite your slot, and a strictly increasing (epoch, timestamp) rule kills replays, checked by every receiver, not just the relay.
  • Removing someone from a circle re-keys it: a fresh chain, mixed with fresh key-exchange entropy, delivered to everyone except the member being removed. They get no wrap, derive no new keys, and cannot follow the circle to its next channel. That is what makes removal complete rather than a request the removed device can just ignore.
  • The relay is a small Cloudflare Worker with a D1 table of ciphertext rows. It knows channel ids, ciphertext sizes, timing, and IPs. It never learns where you are or who your circle is. Rows expire after 24 hours, swept deterministically on every read and write.
  • No push tokens, no analytics, no third party requests. The only external fetch in the whole app is OpenStreetMap tiles, and only when a street basemap is on; the Off-grid basemap renders locally and makes zero requests.
  • Optional app lock encrypts the circle secret at rest behind a passcode (PBKDF2-SHA-256, 600k iterations) and, where the browser supports it, a biometric unlock through the WebAuthn PRF extension. A locked device holds no readable secret in memory or on disk. An optional duress passcode, typed on the lock screen, runs the full panic wipe and comes back up as a fresh install.
  • Places live only on your phone. Name a spot like Home or School and Starling says when someone in the circle arrives or leaves; detection runs on-device against positions that already arrive, so the relay never learns a place exists. SOS, arrival, and low-battery alerts reach the Android app as system notifications while it is in the background, built locally, with no push service involved.

The exact wire format and crypto are in docs/PROTOCOL.md. What the relay can and cannot learn, stated honestly, is in docs/THREAT-MODEL.md. Exactly which of it is wired into a running app today, file by file, is in docs/AUDIT.md. If the code and those files ever disagree, that is a bug.

What it looks like

Dark-first UI. Draggable member sheet, live markers with eased motion, trails, SOS hold-to-fire, check-ins, coarse mode (your device rounds your position to about 1 km before encrypting), panic wipe, and a demo you can run without sharing anything: open starlingmap.app and hit Watch the demo.

An SOS can also mint a help link. Your circle is a list you chose in advance, and the person who can actually reach you may not be on it, so the link opens your live position in any browser with no app and no account, for as long as the emergency lasts. It shows that one emergency: not your circle, not its other members, not any history. Checking in safe switches it off.

Circles are app-only by design. The hosted website is a landing page plus that demo; it refuses to create or open circles, because a browser tab is the weakest place to keep a long-lived location secret (extensions, shared machines, no OS keystore). The full app still runs on localhost for development and testing.

Onboarding The demo Invite App lock

The test/screenshots/ set is regenerated on every end to end run.

Sharing model

Sharing is off until you turn it on, and stopping posts a signed goodbye so your circle sees "stopped" instead of a stale dot. This is live-when-open sharing like Signal's, not an always-on tracker: when the OS suspends the tab, sharing pauses. That is the honest ceiling of the web platform, and the app says so instead of pretending otherwise.

Run it

No build step, no dependencies to install. Needs Node 24 or newer.

# unit tests (crypto, wire, ratchet, rekey, membership,
# relay, QR, UI logic, lock, circles, manifest, and every committed test vector)
node --test test/*.test.mjs

# local dev server (app + relay on one origin)
node test/serve_local.mjs 8899

# the end to end suites (need a real Firefox, and Chromium for the wrapper one; not run in CI)
python3 test/e2e_marionette.py   # sharing: create, invite, join, cross-visibility, check-in, SOS, help link, stop
python3 test/e2e_v2_ui.py        # safety-number comparison, review/accept, re-key, key-change warning, beacon revocation
python3 test/e2e_lock.py         # the app-lock lifecycle, duress included
python3 test/e2e_places.py       # places: save, arrive, rename, reload; the relay never sees one
node test/e2e_wrapper.mjs        # the app-vs-website split and the demo scene

The QR tests cross-check the encoder against the Python qrcode library when it is installed (pip install qrcode); without it those checks skip rather than fail, so a bare clone still runs green.

The e2e suites drive real headless Firefox profiles through the flows named above, dump the relay database at the end, and assert no name and no coordinate appears anywhere in it. Run them yourself rather than trust a claim that they passed on some earlier date; docs/AUDIT.md has the exact commands and what each suite covers.

Deploy

The relay and the app ship as one Cloudflare Worker with static assets. With a Cloudflare API token in CLOUDFLARE_API_TOKEN (Workers Scripts, D1, and Account Settings read), one command creates the database, applies the schema, and deploys:

bash relay/deploy.sh

It is idempotent, so re-running it just ships the latest code. The hosted page serves the landing and demo; sharing itself lives in the Android app. Geolocation needs a secure context, so plain HTTP will not work anywhere.

Redeploying is what actually breaks v1: the relay answers /api/v1/* with 410 Gone rather than syncing an old client into a channel nobody else is on. A v1 client cannot be upgraded in place to talk to a v2 relay, because v1 and v2 derive different channel ids from the same circle secret; every existing circle has to be re-created after a v1-to-v2 relay upgrade. See the changelog entry for 0.5.0.

The relay's rate limits are two vars in relay/wrangler.toml, both per minute: RATE_POST_MIN (writes per channel, default 256) and RATE_GET_MIN (requests per client address, reads and writes, default 240). They are sized so a full 16-member circle sharing normally never meets them, with room for re-key bursts and movement posting; the arithmetic behind each number is in the comments next to them. Raise RATE_GET_MIN if several circles reach you from one NAT, VPN exit or Tor circuit, and RATE_POST_MIN if a whole circle shares while moving. Changing them is a var edit and a redeploy, not a code change.

QR codes

Invite QR codes are generated on-device by a from-scratch byte-mode encoder (versions 1-10, EC level M, all masks). The test suite proves every matrix byte-identical to the Python qrcode library across all mask patterns, so the codes are correct by construction, not by eyeball.

What it does not do

Being clear about the edges is part of the point.

  • It is live-when-open, not an always-on tracker. When the OS suspends the tab, sharing pauses. A wake-lock toggle helps while the screen is on; true background location needs a native app.
  • The relay still sees metadata. It cannot see your position or who you are, but it sees IP addresses, timing, and how many members a channel has. On top of a VPN or Tor this drops to the exit's IP. Firing an SOS is its own correlation signal: the beacon channel and the circle channel update from the same IP at the same instant, even though their keys are unlinkable. And the beacon viewer page opens on a street map immediately, so each helper's browser fetches OpenStreetMap tiles of the emergency's area: the tile host sees the helper's IP and that viewport. See the threat model.
  • Forward secrecy is bounded by the history window, not absolute. Content keys advance every 10 minutes and the old key is destroyed; how much trail stays readable on a device is a setting (10 minutes to 24 hours), and that is the window a compromise can still expose. Post-compromise security comes from re-keying, which has to actually happen: nothing detects a compromise and re-keys automatically.
  • A circle invite still needs a human to say yes. It is no longer a bearer token: a stolen link is inert until the real joiner uses it, and the inviter has to come back online and accept a safety number before any key material changes hands. That is a real cost, not a free upgrade: someone has to be there to say yes.
  • The served page is still a web page. If someone poisons the JavaScript at the origin, that is fatal for whoever loads it, the same as for any web client of any encrypted service. The mitigations are a strict CSP, no third party scripts, and, for the one page the hosted site still serves for security-relevant work (the beacon viewer), published per-release asset hashes that make a targeted swap detectable rather than invisible; see docs/WEB-INTEGRITY.md for exactly what that does and does not buy. The structural fix is that circles only exist in the apps, which bundle their code and never load any from the network: the Android APK, and the iOS wrapper in ios/ (build-from-source today, see docs/IOS.md). The hosted site still refuses to open circles in any browser tab, on every platform, for the same reason.
  • No independent security review. The design is documented before the code, 500-plus unit tests replay committed test vectors, and two rounds of adversarial review plus a cross-model audit have found and fixed real bugs. Nobody outside this project has reviewed any of it. See docs/AUDIT.md.

Android

A native Android app ships with every release and from starlingmap.app. An F-Droid submission is an open, unmerged merge request, so F-Droid has not built or distributed Starling yet; Google Play is in progress and also not live. Today's only distribution is the GitHub release and the direct APK. It runs the same app/ code inside a hand-written Kotlin WebView, and adds what the web platform cannot give it on its own: background sharing through a foreground service (with a persistent notification the whole time, so it is never silent about what it is doing), fingerprint or face unlock through the Android Keystore in place of WebAuthn PRF, a PanicKit responder for panic-button apps like Ripple, and Orbot support. See docs/ANDROID.md for building it, docs/play-listing.md for the Play Store listing, and docs/fdroid/ for the F-Droid submission draft.

A signed APK ships with every release, with a stable starling.apk name that Obtainium can track. The app has no Google services dependency at all (plain LocationManager, no Firebase, no push), so it runs as-is on GrapheneOS and other de-googled Android builds; release testing happens on the no-GMS AOSP emulator image for exactly that reason.

Privacy policy

starlingmap.app/privacy

Roadmap

  • An independent security review. Nothing else on this list matters as much; see docs/AUDIT.md for where to start.
  • More languages. The translation layer is live and Spanish ships with the app; node tools/extract-strings.mjs prints the full catalog for a new language, and a test holds every catalog to full coverage. RTL layout polish lands with the first RTL translation. Native-speaker review of the shipped Spanish is wanted before anything else.
  • F-Droid and Google Play, both still not live; the F-Droid merge request is open.
  • QR scan for safety numbers, alongside the tap-to-enlarge in-person compare that exists today.
  • Per-circle sharing settings (precision, cadence), and being visible to more than one circle at once.
  • Argon2id (memory-hard) app-lock KDF via a vetted WASM build
  • One-time guest links as short-lived side circles

There is an iOS app now: a WKWebView wrapper around the same bundled app, in ios/, that holds a real circle. It is build-from-source only today - a Mac with Xcode, and a free Apple ID re-signs every 7 days - and background sharing does not exist on it, because iOS offers no equivalent of the Android foreground service. docs/IOS.md carries the full capability table and the build steps; TestFlight distribution waits on a paid developer account. What has not changed: circles still do not belong in a browser tab on any platform, and the hosted site still refuses to open them.

License

MIT

About

Private, end-to-end encrypted location sharing for your circle. No accounts, no tracking. Live at starlingmap.app

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages