# Integraties en privacy

Het privacy-hoofdstuk van Tendlo: welke persoonsgegevens waar staan, welke toestemmingslagen er zijn, wat naar derde partijen gaat en waar de AVG-gaten zitten. Dit document vervangt het oude `docs/archive/superseded/2026-07-11-docs-overhaul/docs/2026-06-14-privacy-and-subprocessors.md` (gearchiveerd als [../archive/superseded/2026-07-11-docs-overhaul/docs/2026-06-14-privacy-and-subprocessors.md](../archive/superseded/2026-07-11-docs-overhaul/docs/2026-06-14-privacy-and-subprocessors.md)). Peildatum: 2026-07-11.

> **Geen juridisch advies.** Dit is de feitelijke basis (welke data, welke verwerkers, welke regio's) voor een privacyverklaring en store-privacy-labels. Verwerkingsverantwoordelijke: **Aqua-IT B.V.**

## Kernprincipe: local-first, cloud is een Plus-feature

Standaard staat **alle data lokaal op het toestel** (Drift/SQLite). Zonder cloud-configuratie (`lib/core/config/app_config.dart`) verlaat niets het toestel, op de expliciet genoemde integraties na (weer-API-coördinaten; Health-/device-agenda-reads blijven on-device). Sinds de freemium-omslag (owner-besluit 2026-06-16/17, zie [../business/verdienmodel-en-pricing.md](../business/verdienmodel-en-pricing.md)) is cloud-sync gepositioneerd als onderdeel van de betaalde Plus-tier — niet langer louter een "optionele opt-in": wie Plus met gezinssync gebruikt, gebruikt de cloud. De gratis kern blijft volledig lokaal.

## 1. Welke persoonsgegevens, waar

| Opslagplek | Wanneer | Wat |
|---|---|---|
| **Toestel (altijd)** | altijd | alle domeindata in Drift/SQLite; app-lock-PIN in flutter_secure_storage; instellingen |
| **Supabase EU** (Postgres, project `noeffngomyiglttudamq`, eu-west-1) | alleen cloud-modus | e-mailadres (auth); ledenprofielen per gezinslid — naam/initiaal/kleur/rol, **inclusief kinderen** (`members`); alle 49 gesyncte tabellen, waaronder de gevoelige categorieën **financieel** (12 tabellen: o.a. inkomsten, leningen, bankrekeningen, betaalpassen), **gezondheid** (7 tabellen: o.a. gewicht, medicatie, **cyclusdata**), co-ouderschapsdata en het zakgeld-ledger per kind |
| **Supabase Storage EU** | alleen cloud-modus | bijlagen (foto's/documenten) in de private bucket `attachments`, pad `<householdId>/<ownerId>/<id>.<ext>`; Storage-RLS op het eerste pad-segment (migratie 0004); signed URLs met TTL 1 uur (`lib/core/attachments/data/attachment_storage_service.dart`) |
| **PowerSync Cloud EU** (`powersync_cloud/service.yaml: region: eu`) | alleen cloud-modus | doorvoer/replicatie van de gesyncte rijen |
| **`ai_usage_logs`** (Supabase, server-only) | bij AI-gebruik | household_id, auth_user_id, feature-label, model, tokentellingen en statuscode per AI-call — **géén promptinhoud** (`supabase/functions/ai-proxy/index.ts`, migratie 0011) |

Financieel en gezondheid syncen uitsluitend via de owner-scoped `user_private`-stream — nooit gezinsbreed, tenzij een rij expliciet gericht gedeeld wordt (visibility=2; zie [security-en-rechten.md](security-en-rechten.md)).

## 2. Consent-lagen

- **Analytics is opt-in, default uit.** `ConsentGatedAnalytics` (`lib/core/observability/analytics.dart`) gate't elk event op `AppSettings.analyticsConsent`; toggle in het privacy-scherm (`lib/features/settings/presentation/privacy_screen.dart`). PostHog draait op de EU-host (`eu.i.posthog.com`, default in `app_config.dart`), zonder autocapture/session-replay en met `personProfiles: identifiedOnly` terwijl er nooit geïdentificeerd wordt → uitsluitend anonieme, expliciete events (`lib/core/observability/posthog_analytics.dart`).
- **Crash reporting zonder PII.** Sentry is alleen actief met een DSN (anders no-op); `sendDefaultPii = false`, `tracesSampleRate = 0` (`lib/core/observability/sentry_crash_reporter.dart`).
- **AI kent vier lagen** (`lib/core/ai/ai_gate.dart`): Plus-entitlement → hoofdschakelaar → per-capability-toggle → **sensitive-opt-in**: de domeinen financieel en gezondheid zijn standaard van AI uitgesloten en vereisen expliciete per-domein opt-in (`AiSettings.sensitiveOptInDomains`). Zolang instellingen laden weigert de gate alles (veilige default). Details: [ai.md](ai.md).
- **OS-permissies op het gebruiksmoment**: locatie (weer), camera/fotobibliotheek (scan), microfoon/spraakherkenning, agenda-toegang, Health-toegang en notificaties worden pas gevraagd wanneer de gebruiker de betreffende functie aanzet (zie §5–6).

## 3. Export en verwijdering (rechten van betrokkenen)

- **Export**: gescopede JSON-export via `/backup` (`lib/features/backup/data/backup_service.dart`). De export bevat alleen wat het aanvragende lid mag zien: gezinsbrede rijen, eigen rijen en gericht-met-mij-gedeelde rijen — een backup omzeilt de RLS-/sync-afscherming dus niet.
- **Lokale verwijdering**: het privacy-scherm biedt "alle lokale gegevens verwijderen" (`DataResetService.wipeLocalData`, `lib/features/settings/data/data_reset_service.dart`); in cloud-modus loopt de wipe via `disconnectAndClear` zodat er niets meer geüpload wordt en server-/gezinsdata intact blijft, gevolgd door sign-out (`privacy_screen.dart`).
- **GAT — geen server-side accountverwijdering.** Er bestaat geen delete-account Edge Function (de enige functions zijn `ai-proxy`, `invite-member`, `relocate-attachments`). Een cloud-gebruiker kan zijn serverdata en auth-account dus niet zelf verwijderen; dat kan nu alleen handmatig via de beheerder. Dit is een bekende to-do, ingepland in fase M1 (zie [../product/roadmap.md](../product/roadmap.md)) en moet vóór externe gebruikers gedicht zijn.
- **GAT — geen retentiejobs.** Er is geen automatische opschoning voor `ai_usage_logs`, `merge_audit` of soft-deletes (`deleted_at`); de bewaartermijn is de facto onbeperkt. Ook dit hoort in een retentiebeleid vóór launch.

## 4. Sub-processors

| Verwerker | Doel | Data | Regio | Wanneer actief |
|---|---|---|---|---|
| **Supabase** (project `noeffngomyiglttudamq`) | auth (magic-link), database, storage, edge functions | e-mail, ledenprofielen, gesyncte domeindata, bijlagen | **EU** (eu-west-1) | alleen cloud-modus |
| **PowerSync** | sync lokaal ↔ cloud | gesyncte domeindata | **EU** (bevestigd: `powersync_cloud/service.yaml: region: eu`) | alleen cloud-modus |
| **OpenRouter** | managed AI (LLM-inferentie) | uitsluitend de content die de gebruiker expliciet laat verwerken | **VS** | alleen bij AI-gebruik (Plus, ingelogd); globale spend-cap $30/mnd |
| **Resend** | e-mailbezorging: uitnodigingsmails (`invite-member`, afzender `Tendlo <noreply@tendlo.app>`) + Supabase-auth-mails via SMTP op tendlo.app | e-mailadressen | VS-bedrijf (regio bezorg-infrastructuur niet geverifieerd) | bij invites en auth-mails — **ontbrak in de oude sub-processor-lijst** |
| **PostHog** | productanalytics | anonieme events, geen PII | **EU** (`eu.i.posthog.com`) | alleen na opt-in |
| **Sentry** | crash-/foutrapportage | stacktraces, toestel-/OS-info, geen PII | **EU** (`de.sentry.io`, organisatie `aqua-it-bv-eu`) | bij een crash/fout; DSN staat in de build en is actief |
| **Apple / Google** | betaalrails (IAP), notificatie-OS-diensten n.v.t. (alleen lokale notificaties) | aankoopdata (t.z.t.) | VS/EU | pas bij abonnementen |
| **RevenueCat** | abonnement-entitlements | aankoop-/abonnementstatus | VS | **gepland, nog niet aangesloten** — geen `purchases_flutter` in `pubspec.yaml`; opnemen zodra B5 landt |

Doorgifte buiten de EU (OpenRouter, Resend, t.z.t. RevenueCat — **Sentry niet meer sinds 13 augustus 2026**) vereist passende waarborgen (SCC's) in de privacyverklaring; DPA's per verwerker formeel accepteren blijft een open actiepunt (overgenomen uit het oude privacy-doc).

## 5. Wat gaat er per integratie naar derden — en wat niet

- **OpenRouter (AI)**: uitsluitend content die de gebruiker **expliciet** laat verwerken: gefotografeerde documenten/foto's (base64 in het bericht), gedeelde tekst of opgehaalde pagina-tekst, de assistent-vraag plus gerichte context. Altijd via de server-side `ai-proxy` (key staat nooit in de app); financieel/gezondheid alleen na aparte opt-in. Er worden geen prompts gelogd — alleen tokentellingen (§1).
- **Open-Meteo (weer)**: alleen coördinaten, zonder key of account (`lib/features/weather/data/open_meteo_weather_service.dart`). Default vaste Amsterdam-coördinaten; echte locatie alleen na OS-opt-in via `geolocator`, met stille fallback naar de default bij elke faalmodus.
- **Apple Health en device-agenda**: de **reads zelf** blijven on-device — er gaat geen HealthKit- of agenda-query naar buiten. Health is opt-in per lid, device-lokaal geadministreerd (`lib/features/gezondheid/gewicht/presentation/gewicht_health_providers.dart`); de agenda leest pas na expliciet koppelen (`lib/features/agenda/data/device_calendar_service.dart`). Na een afzonderlijke accountkeuze schrijft de beheerde koppeling ook native events en ondersteunde reeksen naar een eigen Tendlo-kalender (`lib/features/agenda/data/device_calendar_writer.dart`). Titel, tijd en locatie gaan mee; notities alleen na de keuze per bron. Bij iCloud, Google of Exchange kunnen deze kopieën via die provider synchroniseren. De persoonlijke hoofdschakelaar en bronkeuzes begrenzen de export. Gewijzigde toestelkopieën worden bij opruimen niet stil gewist; dubbele wijzigingen en risicovolle reeksvervanging pauzeren als conflict. De gebruiker kiest de Tendlo-versie of behoudt de toestelversie en koppelt die los. Niet-ondersteunde uitzonderingen worden niet geschreven.

  **Maar de afgeleide gegevens synchroniseren wél** (correctie 27-07, eigenaar-besluit: documentatie eerlijk maken in plaats van de sync beperken). Gewichtsmetingen, gewichtsdoelen, dagelijkse stappen en stapdoelen staan in het PowerSync-schema en vier keer in `powersync_cloud/sync-config.yaml`, en gaan in cloud-modus dus naar Supabase (EU). Ze blijven binnen het eigen huishouden en gaan niet naar derden, maar de eerdere formulering *"er gaat niets naar Tendlo-servers"* was onjuist en is hiermee ingetrokken. Wie dit wél on-device wil houden, zet de cloud-sync niet aan — dat is de enige stand waarin de oude belofte klopt. Dit raakt App Store-richtlijn 5.1.3, dus de privacyverklaring moet dit expliciet benoemen.
- **Spraak**: on-device OS-spraakherkenning (`lib/shared/speech/speech_recognizer.dart`); er gaat niets door de AI-proxy. **Kanttekening**: OS-dictatie kan platformafhankelijk cloud-verwerking door Apple/Google inhouden — dat valt buiten de app maar hoort wel in de privacyverklaring.
- **PostHog**: alleen anonieme, grove events na opt-in. **Sentry**: alleen technische foutdata.
- **Resend**: alleen e-mailadres + uitnodigingscontext van invite-mails en auth-mails.

## 6. Overige integraties (permissie-momenten en foutgedrag)

| Integratie | Techniek | Permissie/auth-moment | Foutafhandeling |
|---|---|---|---|
| Notificaties | uitsluitend **lokaal** (`flutter_local_notifications`, `timezone`); géén push/FCM/APNs | OS-notificatiepermissie bij inschakelen (`lib/core/notifications/notification_permission_service.dart`) | budget-planner voor de iOS-cap van 64 pending; quiet hours; module-gate |
| Weer | Open-Meteo REST, key-loos | locatie-permissie alleen bij "gebruik mijn locatie" | elke fout → stille fallback naar default-locatie |
| Device-agenda | `device_calendar_plus`: lezen en schrijven van native events en ondersteunde reeksen in de beheerde Tendlo-kalender | agenda-permissie en expliciete accountkeuze bij koppelen; persoonlijke hoofd- en bronschakelaars | geen writes zonder geldige koppeling/rechten; conflicten pauzeren tot keuze; niet-ondersteunde uitzonderingen blijven ongewijzigd |
| Apple Health | `health`-package, alleen gewicht-import, niet schrijven | Health-permissie op koppel-moment | dedupe met external-id + "skipped uuids"-geheugen tegen resurrectie |
| Share-sheet (in) | `receive_sharing_intent` + iOS ShareExtension (app-group `group.com.aquait.tendlo`) | geen aparte permissie | de-dup tegen dubbele cold-start-aflevering; queue voor payloads vóór login/boot |
| Share-sheet (uit) | `share_plus` (quote delen, backup-export) | geen | n.v.t. |
| Spraak | `speech_to_text` on-device, achter een seam | microfoon-/spraakpermissie bij eerste gebruik | n.v.t. |
| E-mail (Resend) | `invite-member` Edge Function met `RESEND_KEY`-secret | n.v.t. (server-side) | invite-mail is best-effort: mailfout laat de invite zelf intact |
| iCal | `icalendar_parser` voor sportkalender-import (migratie 0018) | geen (gebruiker plakt/kiest feed) | parse-fouten aan de gebruiker gemeld |
| AI-proxy | zie [ai.md](ai.md) | Plus + AI-toggles | OpenRouter-respons wordt verbatim doorgegeven (status + body); 401/429/502-paden expliciet |

## 7. Rechtsgronden (concept, geactualiseerd)

Overgenomen uit het oude privacy-doc (2026-06-14) en aangepast aan de freemium-omslag:

- **Cloud-sync**: uitvoering van de **overeenkomst** (Plus-abonnement) zodra de tiers live zijn; tot die tijd feitelijk toestemming (gebruiker kiest zelf voor inloggen/sync). Intrekbaar door uit te loggen en lokaal te wissen.
- **Analytics**: **toestemming** (opt-in-toggle in-app, intrekbaar).
- **Crash reporting**: **gerechtvaardigd belang** (minimaal, geen PII).
- **Abonnement/betaling** (t.z.t. RevenueCat/stores): **overeenkomst**.
- **Kinderdata**: ouderlijke verantwoordelijkheid binnen het household; geen verwerking buiten de huishoud-functies; verhoogde zorgvuldigheid (kinderen hebben ledenprofielen en zakgeld-/wensenlijst-data).
- **AI-verwerking** (OpenRouter, VS): expliciete handeling van de gebruiker per verwerking + aparte opt-in voor gevoelige domeinen; benoem de VS-doorgifte expliciet in de privacyverklaring.

De checklist voor de uiteindelijke privacyverklaring (verantwoordelijke, doelen, bewaartermijnen, rechten van betrokkenen, AP-klachtroute, store-labels) staat in het gearchiveerde doc en blijft gelden; de store-labels-mapping ("in de lokale modus wordt niets verzameld") blijft een sterk verhaal.

---
*Bronnen: `lib/core/config/app_config.dart`, `lib/core/observability/` (`analytics.dart`, `posthog_analytics.dart`, `sentry_crash_reporter.dart`), `lib/core/ai/ai_gate.dart`, `lib/core/attachments/data/attachment_storage_service.dart`, `lib/features/settings/presentation/privacy_screen.dart`, `lib/features/settings/data/data_reset_service.dart`, `lib/features/backup/data/backup_service.dart`, `lib/features/weather/data/open_meteo_weather_service.dart`, `lib/features/agenda/data/device_calendar_service.dart`, `lib/features/gezondheid/gewicht/presentation/gewicht_health_providers.dart`, `lib/shared/speech/speech_recognizer.dart`, `supabase/functions/` (ai-proxy, invite-member, relocate-attachments), `supabase/migrations/0004`, `0011`, `powersync_cloud/service.yaml`, `pubspec.yaml`, docs/2026-06-14-privacy-and-subprocessors.md (gearchiveerd, 2026-06-14). Geverifieerd op 2026-07-11.*
