De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.
Telemetry and privacy
What the apps measure, what they never send, consent, crash reporting, and the data inventory behind the store privacy labels.
Bijgewerkt: 18 September 2026
Consent first
The last onboarding step asks one question: “Send anonymous usage statistics and crash reports?” with Yes / No, default No. The answer is stored in accounts.telemetry_consent (a boolean the owner may update, default false), so a second device inherits it instead of asking again, and it is changeable in Settings → Privacy. Nothing is sent before that answer. Like Saymail, telemetry is configured after settings have loaded, never in application start-up, so a first launch cannot send anything.
Product analytics (PostHog, EU host)
No vendor SDK (A50). Each platform has a small Telemetry client of its own that batches typed events and posts them to PostHog EU’s /batch endpoint with the project key from INCICO_POSTHOG_KEY (BuildConfig.POSTHOG_KEY on Android, AppConfigGenerated.posthogKey on iOS) and the host from INCICO_POSTHOG_HOST. A blank key disables telemetry completely: the client is never constructed and nothing is queued. The PostHog SDKs bring autocapture, session replay, screen-view capture and lifecycle events, all of which would have to be switched off one by one and could come back with an upgrade; an allow-list is only an allow-list when there is no other path to the network.
Phase 3 emits onboarding_completed and detail_edited, phase 5 service_connected and sync_completed, and phase 6 the remaining four; Android sends all eight.
Only these typed events exist (TelemetryEvents.kt / TelemetryEvents.swift, single definition each, parity-tested):
| Event | Properties |
|---|---|
onboarding_completed | steps_skipped (int), names_count, phones_count, emails_count, addresses_count, dates_count, photos_count |
invitation_sent | via (search / service_cross_reference / share_link) |
invitation_answered | accepted (bool) |
circle_created | total_circles |
detail_edited | category |
service_connected | provider |
sync_completed | provider, added, updated, deleted, duration_ms |
app_error_shown | code |
Every property value is a number, so no event can carry a name or a free text: a choice travels as the position of its value in the list the table gives (via: 0 search, 1 service_cross_reference, 2 share_link; provider: 0 device, 1 google, 2 microsoft), a yes/no as 1/0 (accepted), and category as the detail category’s position (name, phone, e-mail, address, date, photo). app_error_shown.code is the stable number of the i18n key the error banner showed, from an append-only list in TelemetryEvents (1 errors.server, 2 errors.network, …), and 0 for a key not on it; the text itself never leaves the phone.
When each is sent (Android): invitation_sent after the server accepted an invitation from search (Find people → Send invitation) or from a service’s cross-reference (Invite, and each accepted one of Invite all shown); share_link is defined for when invite links arrive (app/14) and not sent yet. invitation_answered after an accept or a decline succeeded (a block is not an answer). circle_created after the server created a circle, with the profile’s number of circles including the new one. app_error_shown once per error key an error banner shows, however often it recomposes.
Never sent: names, e-mails, phones, addresses, dates, photos, free text, ids of other users, exact timestamps of the user’s edits beyond the event time. A unit test asserts every property key against an allow-list and every value type against a schema. The distinct id is the install id, not the account id, so events cannot be joined to a person by PostHog.
Crash reporting
Uncaught exceptions are captured with the stack trace (callStackSymbols / Throwable.stackTrace), app version, OS version and device model. The exception message is not sent, because a message can contain a value (Saymail learned this). Symbolication happens server-side with the uploaded mapping file (Android) and dSYM (iOS). Crash handlers always delegate to the previously installed handler so Play Vitals and Xcode Organizer keep working as an independent check.
On Android (A89) there is no crash SDK: the handler goes in the first time consent is true in a process, chained in front of the handler already there, and asks for consent again when it fires. It writes noBackupFilesDir/crash.json synchronously on the crashing thread — the class of the exception and of up to three causes, each with up to 64 frames of class and method name (no line numbers, as A89 rules, and never the message) — plus the app version and build, the Android release and the device’s manufacturer and model, and then hands the exception on. The next start waits for the cached account row: with consent the file goes out as one $exception event ($exception_list with each exception’s type and raw Java frames, outermost first and the crash site last, each carrying the build’s R8 map id as map_id in a release build so PostHog replaces the obfuscated names with the uploaded mapping (Build and release); the id is recorded with the crash, so a report sent by the next version still names the mapping of the build that crashed; $exception_level fatal, $app_version, $app_build, $os, $os_version, $device_model) through the same PostHog client and install id as the events, and is deleted once PostHog took it; a failed send keeps it for the start after; without consent it is deleted unsent. A build without a PostHog key deletes it too.
Diagnostics log
A local ring buffer (last 2 000 lines, in the app sandbox, excluded from backup) records network status, sync steps and error codes. It never contains detail values; log call sites are grepped in CI for the words phone, email, address next to interpolation. The user can view and share the log from Settings → Help → Diagnostics, which is the successor to v1’s hidden long-press log viewer, now discoverable.
On Android (A88) every log call goes through one AppLog facade, which prints to logcat as before and appends each line from INFO up (debug lines stay in logcat) to noBackupFilesDir/diagnostics.log as time level tag: message, one line per call, from a background thread; the file is cut back to the newest 2 000 lines when it passes 2 200. Before a line goes anywhere the facade replaces e-mail addresses, phone-like runs of seven or more digits, JWT-like and bearer tokens and secure-store key names with markers, and withholds a line that assigns a password, secret or token as a whole; a throwable is written as its class chain (with an app error’s stable reason code), never its message. A unit test runs these patterns through the facade, and tools/log-check.mjs fails CI when a log call’s string template interpolates a sensitive name (phone, email, address, e164, token, password, secret, jwt, otp, a person’s name) or when a file other than the facade imports android.util.Log. The diagnostics id shown in Help and put into support mails is a random UUID per install, separate from the telemetry install id, which support never needs.
Server-side
Supabase’s own logs (Postgres, PostgREST, Auth, functions) are retained 7 days on the plan in use. audit_events (see Data model) is the application-level record. No third-party analytics run in edge functions.
Website
The website carries no analytics or advertising scripts at launch and therefore needs no cookie banner. Netlify access logs are the only record. If analytics are added later, the Saymail consent-banner pattern is reused.
Data inventory for store labels
| Data | Collected | Linked to user | Purpose | Retention |
|---|---|---|---|---|
| E-mail (sign-in) | yes | yes | account | until deletion |
| Name(s), phone(s), e-mail(s), address(es), dates, photos | yes, user-entered | yes | app functionality (sharing with chosen circles) | until deletion; backups ≤ 30 days |
| Connections and circle assignments | yes | yes | app functionality | until deletion |
| Device address book | read on device only when the service is connected | no (never uploaded) | sync | not stored server-side |
| Google Contacts | read/written on device with the user’s own token | no | sync | token on device only |
| Push token, platform, app version, locale | yes | yes | notifications | until sign-out or token invalidation |
| Install id, typed usage events | only with consent | no (install id) | analytics | 12 months |
| Crash traces | only with consent | no | stability | 90 days |
| Hashed IP in audit events | yes | yes | abuse prevention | 12 months |
No data is sold, no advertising identifiers are used, and no tracking across apps or sites takes place. The Apple NSPrivacyTracking flag is false; the Play Data Safety form marks all items as not shared with third parties except processors (Supabase, Google/Apple push).