Insights — internal
Enter the password to continue.
Insights — GDPR review

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.

Uman C AB · controller · verified 17 Aug 2026 · nothing changed in production

5Must fix
7Should fix
6Verified fine
10Processors
241Data subjects
⚠ The single most serious finding. The "Delete account" button does not delete the account. 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.
Method

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.

Compulsory

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)

SupabaseAuthProvider.swift:164 · AuthControllerImpl.swift:194 · routers/user.py:417

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.

Fix: delete the auth user server-side with the service key (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)

routers/user.py:428–437

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.

Fix: collect per-table results, fail loudly if any delete errors, and write an erasure receipt (user id hash, timestamp, tables cleared) to an audit table. Accountability under Art. 5(2) means being able to show it happened.

3. Anthropic is an undisclosed processor receiving users' own words art. 13 art. 28

routers/deeper.py:101 · routers/recommend.py:200

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.

Fix: (a) sign a DPA with Anthropic; (b) name them in the policy as a processor with the purpose and the US transfer; (c) confirm the API terms exclude training on your data. Until (a) and (b) are done, this is the clearest disclosed-processing gap on the list.

4. No consent gate on analytics or advertising art. 6 ePrivacy

grep for an analytics consent gate returns nothing

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.

Fix: gate Segment/Meta initialisation behind an explicit consent choice for EU users, defaulting to off, and record the choice with a timestamp. This is a real product decision — it will reduce measured volume — so it is yours, not mine.

5. The privacy policy is materially incomplete and 17 months old art. 13

join-insights.com/privacy-policy · "effective as of 3.3.2025"

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.

Fix: rewrite against the processor table below. I can draft it, but it is user-facing text and therefore yours to approve — and worth a legal read before publishing.
Recommended

Should fix

6. The data export is missing six tables art. 15 art. 20

routers/user.py:380

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.

Fix: a one-line-per-table addition, and worth deriving both lists from one shared constant so they can never drift again. Safe for me to do on a branch on your say-so.

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.

Fix: a "Download my data" row in Settings next to "Delete account". Small change; needs your call on placement and wording.

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.

Fix: decide the dormancy window, then a scheduled purge. Note the promise is already public, so today the policy overstates what happens.

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.

Fix: add the manifest to the app target. Mechanical; I can write it from the data inventory below.

10. Notification consent is hardcoded true art. 7

InsightsApp.swift:309

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.

Fix: send the user's current stored preference rather than a literal 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.

Fix: add it to the erasure list now, before it matters.

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.

Fix: the data inventory below is a ROPA starter. The DPIA needs a real decision on whether inferred psychological profiling is Art. 9 processing — that is a question for a DPO, not for me.
Verified

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.

Where it goes

Personal data out of the EU

The database stays in Ireland. Almost everything else leaves.

iOS appSupabase Ireland 🇪🇺 | iOS appRailway USAnthropic US | iOS appSegmentAmplitude+Customer.io+Meta
ProcessorWhat it receivesRegionDPAIn the policy?
SupabaseAll personal data — the primary store🇪🇺 Irelandconfirmno
RailwayEverything in transit — the API processes all of it🇺🇸 US-eastconfirmno
AnthropicUsers' own reflective text + card context🇺🇸 USneededno
SegmentAll behavioural events + identify traitsconfirmconfirmno
AmplitudeBehavioural events, user id, 13 traitsconfirmconfirmyes
Customer.ioEmail, push token, lifecycle traitsconfirmconfirmyes
MetaApp events, advertising identifiers🇺🇸 USconfirmno
RevenueCatSubscription state, app user id🇺🇸 USconfirmno
AppleSign-in, payments, push delivery🇺🇸 USstandardyes
CloudflareShare/deep-link traffic metadataedgeconfirmno
Note on the DSAR address. The policy directs rights requests to 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.
Article 30

Data inventory — a ROPA starter

Every table holding personal data, with row counts verified live on 17 Aug 2026.

TablePersonal data heldRowsErased?Exported?
auth.usersAccount id, email address241nono
user_profilesIntent embedding, consent flags, push token139yesyes
user_signalsFree-text check-ins and reflections + embeddings40yesyes
card_interactionsEvery engagement event, timestamped4,492yesyes
served_cardsWhat was shown, when1,483yesyes
favoritesSaved cards92yesyes
user_self_modelInferred psychological portraityesno
user_clustersNamed themes inferred from reading113yesno
user_card_linksCard-to-card links personal to the useryesno
user_path_progressPosition in each journey308yesno
user_stats_snapshotsHistoric engagement statsyesno
prepared_cardsPre-assembled personal decks334yesno
referral_codes.claimed_byUser id against a claimed code0 claimednono
Device (UserDefaults)Notes and reminders the user wrote — never syncedon-deviceyesn/a
On the on-device notes. They never reach the server, so they are outside a server-side export by definition — but they are also lost forever when a user changes phone, which is a data-loss problem before it is a privacy one. Worth deciding deliberately: sync them (and bring them into export and erasure), or tell users plainly they are device-only.
Next

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.

Your feedback on this page