Skip to content

How it works ​

A walkthrough of the tracking pipeline, from tap to POST.

Startup ​

main() runs three things in order before runApp:

  1. Storage.getInstance() — initializes SharedPreferences and resolves the app documents directory (where connections.json lives).
  2. LocationService.ready() — configures Tracelet with the balanced profile, a 300m distance filter, stopOnTerminate: true, startOnBoot: false, and platform-specific foreground-service / background-indicator config.
  3. 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:

  1. Save last location — Storage.writeLastLocation(entity) persists the point to SharedPreferences so the map screen can show it.
  2. Compute the active set — filter Connections to those that are enabled and within [start, end].
  3. Fan out POSTs — PostService.fanOut issues one fire-and-forget POST per active Connection. Each POST uses the shared Dio instance with 10s/10s/15s timeouts.
  4. Update status — each POST reports success or failure back to the connectionStatusProvider via a monotonic per-connection requestId that drops stale out-of-order updates.
  5. 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.

The App supports two deep-link schemes:

  1. Custom scheme — withtracker://connection?c=<base64-JSON>. Works everywhere, no domain or server required. Registered in Info.plist (iOS) and AndroidManifest.xml (Android).
  2. 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 serve apple-app-site-association (iOS) and .well-known/assetlinks.json (Android) at the root — both are published from this site's public/ folder.

Both variants use the same base64-JSON config payload and the same parser.

On open:

  1. deepLinkProvider parses and validates the config via ConnectionConfig.fromJson.
  2. On success, it navigates to /connections/add with the parsed payload as go_router extra, opening the Connection Form pre-filled.
  3. On failure (bad base64, invalid JSON, or validation error), it navigates to the Connections screen and shows an error dialog with the specific message.
  4. 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:

DependencyRoleNetwork
traceletLocation recording (GPS, motion, foreground service)None (sensors only)
dioHTTP client for POSTing to user webhooksOnly user-configured URLs
flutter_map + OSM tilesMap renderingOSM tile servers
app_linksDeep-link ingestionNone (reads incoming links)
tracelet_doctorDebug 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.

Made by Eagle Logistics.