Insights — GDPR review & remediation
An obligation-by-obligation audit against the live code, the live database and the published privacy policy — not against design documents. Every finding below was verified, and the things that turned out fine are recorded as carefully as the things that did not.
SupabaseAuthProvider.deleteAccount() calls client.auth.signOut() and nothing else — the row in auth.users, including the email address, survives permanently. The app data is deleted; the identity is not. This is an Art. 17 failure and also breaches Apple App Store Guideline 5.1.1(v), which requires apps offering account creation to offer account deletion. It is a review-rejection risk as well as a legal one.
How this list was built
Each obligation was checked against something real, and the check is named so you can re-run it. Where something could not be verified from here, it is marked confirm rather than asserted.
Against the live database
Table-by-table inventory, every column that keys to a person, RLS state and policy definitions, row counts — queried directly against Supabase Ireland bkiodocaqdgtkaixoisn.
Against the live code
Both repos read directly: the erasure and export handlers line by line, the consent write, the iOS delete flow down to the auth provider, and every outbound call carrying user text.
Against the published policy
The live privacy policy at join-insights.com/privacy-policy was fetched and compared line-for-line with what the code actually does.
What is not covered
No legal advice — this is an engineering audit that maps findings to articles. A DPO or solicitor should sign off the ROPA, DPIA and the policy rewrite. ICO/IMY registration status was not checked.
Must fix
Ordered by exposure. The first three are the ones I would not run paid acquisition behind.
1. "Delete account" is a sign-out art. 17 apple 5.1.1(v)
The flow is: iOS calls DELETE /v1/user/data (which does erase the ten app-data tables), clears local storage, then calls authProvider.deleteAccount() — which is just signOut(). The backend's own response says it plainly: "Your account authentication remains intact. To fully delete your account contact support." The user is told the account is deleted; the identity row and email remain.
auth.admin.delete_user) as the final step of DELETE /v1/user/data. Every user table already has ON DELETE CASCADE to auth.users, so the cascade does the rest — the explicit loop becomes belt-and-braces. Then correct the response message. Not applied — this deletes production identities and is yours to approve.2. Erasure reports success even when it fails art. 5(2)
Every table delete is wrapped in try / except: pass, and the handler returns {"success": true} unconditionally. A partial or total failure is indistinguishable from a clean erasure. You cannot evidence compliance, and the user is told something untrue.
3. Anthropic is an undisclosed processor receiving users' own words art. 13 art. 28
Go-deeper sends the user's typed messages to Anthropic's API, together with the card they are reflecting on. That text is reflective, personal, and often about the user's inner life. A second path in recommend.py sends engagement context to Haiku. The privacy policy does not mention Anthropic, Claude, or AI processing at all.
4. No consent gate on analytics or advertising art. 6 ePrivacy
Segment initialises at launch and fans out to Amplitude, Customer.io and Meta regardless of any choice the user makes. The only consent toggles that exist are email and notifications. The policy claims Legitimate Interest for analytics — for EU users, analytics and especially advertising SDKs generally require consent, and Meta's App Events are advertising.
5. The privacy policy is materially incomplete and 17 months old art. 13
It names only Amplitude, Customer.io and Apple. Missing: Anthropic, Segment, Meta/Facebook, RevenueCat, Supabase and Railway — including the database that holds everything and the US host that processes it. It does not name the controller's legal entity. It predates go-deeper, the swipe deck and the self-model entirely. It states a two-year retention period that is not implemented anywhere in the code.
Should fix
6. The data export is missing six tables art. 15 art. 20
GET /v1/user/data exists and is honest about free text — it exports profile, interactions, served cards, favorites, notes and signals. But erasure deletes ten tables and export returns four of them. Missing: user_self_model, user_clusters, user_card_links, user_path_progress, user_stats_snapshots, prepared_cards. The self-model is arguably the most personal thing you hold — it is an inferred portrait of the person.
7. There is no way to request your data in the app art. 12
The export endpoint has no UI. Nothing in Settings calls it. In practice a user must email to exercise a right that is already built and one screen away from working.
8. The stated retention period is not implemented art. 5(1)(e)
The policy promises deletion "up to 2 years following account termination". There is no retention or purge job anywhere in the codebase — no scheduled deletion, no dormancy sweep. Data is currently kept indefinitely. The two nightly jobs only prepare cards and rebuild self-models.
9. No app privacy manifest apple
No PrivacyInfo.xcprivacy exists for the app target — the only ones present belong to third-party SDKs (Segment, RevenueCat, Facebook, Lottie). Apple requires the app's own manifest declaring collected data types and required-reason API use.
10. Notification consent is hardcoded true art. 7
The APNs-token callback posts notificationConsent: true unconditionally. It is defensible — a token is only issued after the user grants the OS permission — but it also overwrites any later explicit choice each time the token refreshes.
true.11. Erasure misses referral_codes.claimed_by latent
That column holds a user id with ON DELETE SET NULL, and it is not in the erasure loop. Zero of 500 codes are claimed today, so there is no live residual — but it becomes one the moment redemption ships.
12. No ROPA, no DPIA, and special-category exposure is unconsidered art. 30 art. 35
Neither document exists. And the question has not been asked: the self-model infers psychological patterns from what someone reads about trauma, attachment, desire and shame, and user_signals stores their free-text reflections. That is at least adjacent to health data under Art. 9, and profiling under Art. 22 deserves a look. The policy never mentions special-category data.
What is already right
Recorded with the same care as the failures — this is genuinely better than the average app at this stage.
✓ Row-level security is properly done
RLS is enabled on all 26 tables. Every user table's policy is auth.uid() = user_id — correct isolation, no gaps. Eleven tables have RLS on with no policy, which is deny-by-default, i.e. fail-safe.
✓ Erasure covers every user-keyed table
All ten tables carrying a user_id are in the deletion loop. Nothing personal is left behind in the app schema (the claimed_by column above is the one edge, and it is empty).
✓ The primary store is in the EU
Supabase Ireland is the source of truth for all personal data. That is the right default and removes the hardest transfer question for the database itself.
✓ The iOS delete flow is carefully ordered
It only signs out and clears local data after the backend returns success, and keeps the user signed in on failure so they can retry — deliberately avoiding the "told it was deleted, but it wasn't, and now I can't retry" trap. The logic is right; it is let down by the backend always returning success.
✓ Local data is wiped on delete and sign-out
clearPerUserData() broadcasts a clear that empties the on-device notes store in memory and on disk, so a second user on the same device cannot see the first user's notes.
✓ The consent write is treated as critical
The consent upsert is wrapped in safe_execute with an explicit comment that a transient failure must not silently lose a GDPR-relevant choice. Someone was thinking about this.
Personal data out of the EU
The database stays in Ireland. Almost everything else leaves.
| Processor | What it receives | Region | DPA | In the policy? |
|---|---|---|---|---|
| Supabase | All personal data — the primary store | 🇪🇺 Ireland | confirm | no |
| Railway | Everything in transit — the API processes all of it | 🇺🇸 US-east | confirm | no |
| Anthropic | Users' own reflective text + card context | 🇺🇸 US | needed | no |
| Segment | All behavioural events + identify traits | confirm | confirm | no |
| Amplitude | Behavioural events, user id, 13 traits | confirm | confirm | yes |
| Customer.io | Email, push token, lifecycle traits | confirm | confirm | yes |
| Meta | App events, advertising identifiers | 🇺🇸 US | confirm | no |
| RevenueCat | Subscription state, app user id | 🇺🇸 US | confirm | no |
| Apple | Sign-in, payments, push delivery | 🇺🇸 US | standard | yes |
| Cloudflare | Share/deep-link traffic metadata | edge | confirm | no |
hello@Insights-app.com — a different domain from the product (join-insights.com). I checked: insights-app.com does have live MX records at IONOS, so it is not a dead address. But it is separate mail infrastructure from your Google Workspace. Worth sending a test request to confirm someone actually reads it — a missed DSAR is a one-month clock you never knew started.Data inventory — a ROPA starter
Every table holding personal data, with row counts verified live on 17 Aug 2026.
| Table | Personal data held | Rows | Erased? | Exported? |
|---|---|---|---|---|
| auth.users | Account id, email address | 241 | no | no |
| user_profiles | Intent embedding, consent flags, push token | 139 | yes | yes |
| user_signals | Free-text check-ins and reflections + embeddings | 40 | yes | yes |
| card_interactions | Every engagement event, timestamped | 4,492 | yes | yes |
| served_cards | What was shown, when | 1,483 | yes | yes |
| favorites | Saved cards | 92 | yes | yes |
| user_self_model | Inferred psychological portrait | — | yes | no |
| user_clusters | Named themes inferred from reading | 113 | yes | no |
| user_card_links | Card-to-card links personal to the user | — | yes | no |
| user_path_progress | Position in each journey | 308 | yes | no |
| user_stats_snapshots | Historic engagement stats | — | yes | no |
| prepared_cards | Pre-assembled personal decks | 334 | yes | no |
| referral_codes.claimed_by | User id against a claimed code | 0 claimed | no | no |
| Device (UserDefaults) | Notes and reminders the user wrote — never synced | on-device | yes | n/a |
Who does what
I can do on your say-so
Branch work, nothing live: real account deletion in the erasure handler · make erasure report honestly + write a receipt · add the six missing tables to export (from one shared constant) · add claimed_by to erasure · stop hardcoding notification consent · write the app privacy manifest · draft the ROPA from the inventory above.
Needs your decision
Whether analytics/advertising gets a consent gate for EU users (it will cut measured volume) · the retention window · whether to sync notes or declare them device-only · where a "Download my data" row goes in Settings · approving the rewritten privacy policy.
Needs someone who isn't me
DPAs with Anthropic, Supabase, Railway, Segment, Amplitude, Customer.io, Meta, RevenueCat, Cloudflare. Whether inferred psychological profiling is Art. 9 processing — a DPO or solicitor call. Supervisory-authority registration. A legal read of the rewritten policy before it goes up.
Do first
If you only do three things: make delete actually delete (legal and App Store), disclose Anthropic, and fix the policy. Those three close the gap between what users are told and what happens — which is what regulators and reviewers actually look at.