Theme
How it works
A walkthrough of the tracking pipeline, from tap to POST.
Startup
main() runs three things in order before runApp:
Storage.getInstance()— initializesSharedPreferencesand resolves the app documents directory (whereconnections.jsonlives).LocationService.ready()— configures Tracelet with thebalancedprofile, a 300m distance filter,stopOnTerminate: true,startOnBoot: false, and platform-specific foreground-service / background-indicator config.runApp(ProviderScope(child: WithTrackerApp())).
appInitProvider (a FutureProvider) gates the UI behind a loading screen until init completes. A deepLinkProvider listens for withtracker:// links and queues any that arrive during init.
The tracking pipeline
On each recorded location, in order:
- Save last location —
Storage.writeLastLocation(entity)persists the point toSharedPreferencesso the map screen can show it. - Compute the active set — filter Connections to those that are
enabledand within[start, end]. - Fan out POSTs —
PostService.fanOutissues one fire-and-forget POST per active Connection. Each POST uses the sharedDioinstance with 10s/10s/15s timeouts. - Update status — each POST reports success or failure back to the
connectionStatusProvidervia a monotonic per-connectionrequestIdthat drops stale out-of-order updates. - Re-evaluate tracking state — if there are active Connections, the Android foreground service is enabled (background tracking survives app backgrounding). If there are none, it is disabled (foreground-only — GPS turns off when the app is backgrounded).
Foreground service (Android)
The foreground service and its notification are tied to whether there are active Connections:
- Active Connections present → foreground service enabled, notification shown ("With Tracker — tracking active"). Background tracking + POSTing survives app backgrounding.
- No active Connections → foreground service disabled. Tracking runs foreground-only; GPS turns off when the app is backgrounded.
When tracking is stopped (intent off), the service stops and the notification disappears. Tracelet manages the notification channel.
On iOS, the OS handles background location independently — there is no foreground service. A blue status-bar pill / Dynamic Island indicator is shown while tracking in the background.
Connection status
Each Connection has a live status held in memory only (not persisted):
lastSuccessAt,lastAttemptAt,consecutiveFailures,lastError.- Derived
health: green (active), grey (inactive), red (broken). - When broken, the last error shows as a red subtitle on the Connections screen row (e.g. "401 Unauthorized — 2 min ago").
Status is joined to the Connection list by id in the UI via a ConnectionWithStatus view model — the join happens once in the provider layer, not in every build.
Storage
All persistent state is on-device, in the app's private sandbox:
connections.json— the Connections list, written atomically via.tmp+ rename. Tokens are stored in plaintext (encryption at rest is deferred).SharedPreferences— the last location (one JSON string) and the tracking intent flag.
There is no database, no sync engine, no on-device queue. Points are delivered in real time or not at all.
Deep links
The App supports two deep-link schemes:
- Custom scheme —
withtracker://connection?c=<base64-JSON>. Works everywhere, no domain or server required. Registered inInfo.plist(iOS) andAndroidManifest.xml(Android). - Universal Links / App Links —
https://withtracker.com/open/connection?c=<base64-JSON>. Opens the App directly when installed, falls back to the website when not. Requires the domain to serveapple-app-site-association(iOS) and.well-known/assetlinks.json(Android) at the root — both are published from this site'spublic/folder.
Both variants use the same base64-JSON config payload and the same parser.
On open:
deepLinkProviderparses and validates the config viaConnectionConfig.fromJson.- On success, it navigates to
/connections/addwith the parsed payload asgo_routerextra, opening the Connection Form pre-filled. - On failure (bad base64, invalid JSON, or validation error), it navigates to the Connections screen and shows an error dialog with the specific message.
- The user reviews and confirms before anything is saved — deep links never auto-import.
If a link arrives during init, it is queued and processed after init completes. If multiple links arrive in quick succession, only the latest is processed.
Dependency map
What touches the network or sensors, and why:
| Dependency | Role | Network |
|---|---|---|
tracelet | Location recording (GPS, motion, foreground service) | None (sensors only) |
dio | HTTP client for POSTing to user webhooks | Only user-configured URLs |
flutter_map + OSM tiles | Map rendering | OSM tile servers |
app_links | Deep-link ingestion | None (reads incoming links) |
tracelet_doctor | Debug diagnostics (kDebugMode only) | None |
Everything else (permission_handler, shared_preferences, path_provider, url_launcher, share_plus, package_info_plus, file_picker, uuid, go_router, flutter_riverpod, cupertino_icons) is local-only. See the privacy policy for the full audit.
