What's done, what's still open, and who it's waiting on — one table to scan, details below for anyone who wants the why/where/how. Sourced from GDPR, UK-specific law, Apple's App Store rules, or Allin's own Terms/Privacy Policy (tagged per item) — checked against live code and dashboards, not the intended design. Not a solicitor's sign-off; a lawyer should confirm the borderline calls.
We want to make this practice better,
your choice, for each of these.
privacy & data
PrivacyConsentViewAnalyticsClient/MetaAdsClient check stored consent before sending anything — not send-first-hope-they-opt-out-later.
identify().deleteAccount()recommend-plan was documented as "FTS only, no LLM" — wrong. The live app calls Claude/Anthropic on every plan generation (FTS is the fallback). Now disclosed in the DPIA, ROPA, and privacy policy draft; a signed DPA with Anthropic is the top open item as a result.| Processor | Status | What's left |
|---|---|---|
| Apple | Done | Covered by the Developer Program License Agreement. |
| Supabase | Done | Auto-incorporated into its Terms — confirmed directly in the dashboard, no signature needed. |
| Amplitude | Done | Same auto-coverage, confirmed at their Trust Center. |
| Customer.io | Found | Needs actual signing (not automatic) — Marie, from her other account. |
| Meta | Not started | Marie — paused deliberately, not blocked. |
| RevenueCat | Blocked on login | Marie — also needs a new v2 API key while logging in. |
| Anthropic | Blocked on login | Marie — platform.claude.com needs its own separate login. |
| Segment | Not checked | Lower urgency — never receives special-category text. |
dormant_accounts() identifies accounts past this cutoff — identification only, no auto-delete yet.
you don't have to hold this alone
A new version is available
presentReferralShare()record_consent_granted was never wired to a real table — the call has been silently failing since the granular screen shipped. Confirmed directly against the live database: 0 of 112 real users have any consent recorded. Fix is built: a proper table plus matching functions for both the initial grant and later Settings changes.
export-user-data returns everything Allin's own tables hold, as JSON, via a new "download my data" row in Settings. dormant_accounts() just identifies accounts past the 180-day cutoff — no deletion or notification yet.
exportMyData() · allin-backend · feat/gdpr-export-retentionEach item below has a ready prompt — paste it into your own Claude.
recommend-plan. That's the actual basis for calling this reasonably complete — systematic obligation-by-obligation coverage, verified against what's really running, not a checklist copied from a template. Honest limit: this is a technical/product review, not a solicitor's sign-off — the borderline calls (age-gate, ICO specifics) are worth a real lawyer confirming before treating this as final.The old flow was one low-friction button — "allow tracking and reminders" or "skip" — covering multiple different purposes (analytics, advertising, reminders) with a single yes/no. GDPR requires consent to be specific: each distinct purpose needs its own separate, informed, freely-given choice, not one blanket switch. That's why this became a genuine 3-decision screen, each toggle gettable and revocable on its own. One thing still genuinely unresolved: there's no separate decline/skip button — leaving every toggle off and tapping "continue" is the decline path, so someone who says no to everything sees the exact same button as someone who said yes. Worth your read on whether "continue" reads neutral enough either way.
GDPR Art. 7(3) requires withdrawal to be as easy as granting — a one-time consent screen alone doesn't satisfy that. This is the permanent version inside Settings, so changing your mind later isn't buried or harder than the original choice was.
Built alongside the consent redesign, and arguably the most important piece: AnalyticsClient/MetaAdsClient now check the stored consent state before sending anything at all, rather than sending first and hoping someone opts out later. The consent screen only matters if a "no" actually stops something from happening — this is the part that makes that true day to day, separate from being able to prove it happened (see the consent audit trail below).
Amplitude needs a persistent reference to a person for behavioral analysis, but never needed their actual email — sending it anyway was excess data under the minimization principle (Art. 5(1)(c)), and was the single most urgent, fastest fix found across every audit on this project. Fixed by removing the Segment mapping that forwarded email as a User Property, then independently re-verified against the app's real analytics code to confirm it's actually gone, not just configured-to-be-gone. The only item on this page confirmed live in production, not just committed.
A real problem found while reviewing the erasure flow: if deletion failed partway, the failure was silently swallowed and the app proceeded exactly as if it had succeeded — meaning someone could believe they'd exercised their right to erasure while their account and data were still fully live. Pretending success is arguably worse than a visible error, since it removes the person's ability to even know something went wrong and try again. Also fixed alongside this: consent is per-account, not per-device, so it's now explicitly cleared on delete (not on plain sign-out) — otherwise a deleted account's consent could silently carry over to whoever signs up next on that device.
recommend-plan had been documented as "100% Postgres full-text search, no LLM call" — that was wrong. The live app activates a Claude/Anthropic composition path on every plan generation; full-text search is the fallback only. That makes Anthropic a real processor, receiving check-in and plan-intake text, that wasn't previously disclosed anywhere. Added to the DPIA, ROPA, and privacy policy draft the moment this was found — and a signed DPA with Anthropic became the single highest-priority open item as a direct result.
The foundational accountability paperwork Arts. 30/35 require for processing this kind of data at all — a data protection impact assessment and a record of processing activities. Both complete, and kept current as new processors were found rather than treated as a one-time exercise.
Distinct from the GDPR paperwork above — this is Apple's own submission requirement (declaring data collection and "required reason" API usage), enforced at app review, not a GDPR obligation. Was previously stale; corrected to match actual ATT-gated tracking behavior.
GDPR Art. 28 requires a written agreement with anyone processing personal data on Allin's behalf — this isn't scaled by sensitivity or company size, it applies the moment a third party touches personal data at all. Apple is covered by the existing Developer Program License Agreement. Supabase and Amplitude were checked directly in each dashboard (2026-08-17) and confirmed auto-incorporated into their respective Terms of Service — no signature needed, no action required. See the table above for what's still open on Customer.io, Meta, RevenueCat, and Anthropic.
GDPR Art. 5(1)(e) requires a defined limit on how long personal data is kept — before this there was no policy at all, just indefinite storage by default. Marie's decision: 180 days of inactivity, matching Segment's own retention window for the same account, rather than an unrelated number. dormant_accounts() identifies accounts past this cutoff — identification only for now, not auto-deletion (see the nice-to-have below for turning this into a full lifecycle).
Not a GDPR item, but the same class of accuracy problem as the age-gate promise below: a stated price that doesn't match what's really charged. Was a genuine mismatch (£69 vs. the real £69.99), confirmed live 2026-08-14; re-checked directly 2026-08-19 and the site now correctly states £69.99 — fixed independently of this project, no action needed.
Four smaller pieces, not required by GDPR but part of the same overall safety and honesty posture: the crisis-support screen now offers Shout (text) alongside Samaritans (call) for UK users, since not everyone in crisis wants to make a phone call. ForceUpgradeView was restyled since it's the one screen most likely to actually stop a real user and previously looked like a generic placeholder. The review and referral prompts intentionally use Apple's native system UI rather than a custom screen — nothing further to build there.
Account deletion previously only removed Allin's own Supabase rows — it never told Segment, Amplitude, or Customer.io, so a "deleted" person's data kept living indefinitely in every third-party processor, which doesn't satisfy the right to erasure. This function fans erasure requests out to all three in parallel, then calls the existing delete_user() database function. A processor failure is logged but deliberately non-fatal — trapping someone in a "we couldn't delete your account" loop because a third-party API had a bad minute would be a worse GDPR posture than proceeding, and none of these processors delete synchronously anyway (their own stated SLAs are "within 30 days," not instant). Meta is deliberately not called here — they don't expose an API for requesting erasure of an arbitrary user, only the reverse; Allin's actual control over Meta data is upstream, in what it collects and the consent gate already in place.
GDPR Art. 7(1) doesn't just require that consent was actually given — it requires being able to demonstrate it was, on demand. The app's own protective behavior (not sending analytics/ads without consent) was already fine, enforced separately client-side — what was missing was proof. This was discovered by reading the consent screen's real server-side behavior rather than trusting that a function with a matching name meant a working implementation: the call was wrapped in a silent try?, failing with zero visible error since the granular screen shipped. Confirmed directly against the live database: 0 of 112 real users have any consent recorded at all. The fix is a proper table plus matching functions for both the initial grant and later Settings changes.
This came directly out of a real incident, not a theoretical audit: a name was found sitting next to a private note on a live dashboard query. The redacted-views mitigation (next item) only protects when someone logs in through the restricted role — it does nothing to stop the owner or service-role login from reading identity and content together directly, which is exactly the access level that produced the incident. Getting to "even a full-privilege login sees a random token, not a person" needs the account id actually separated from the content, which is why this became staged, multi-part work rather than a single fix. Stage 1 is additive only — a mapping table and trigger, nothing existing can break.
Pseudonymization Stage 2/3 needs a watched maintenance window and a backup before it can run — it isn't a same-night fix. This was the fast, safe thing to do in the meantime: three SQL views exposing only coarse metadata (truncated id, mood-shift direction — never free text or an email), plus a restricted login role, to reduce the everyday risk of someone glancing at a dashboard and seeing a name next to a private note. Explicitly a stopgap, not a replacement — it doesn't stop a privileged/owner login, which is exactly what pseudonymization is for.
Two separate hard requirements bundled into one build pass. Arts. 15/20 give someone the right to access and receive a copy of their own data — until export-user-data existed, Allin had no way to honor that request at all. Art. 5(1)(e) requires a retention limit — dormant_accounts() is the identification half of that (see the Done retention card above for the decided 180-day number).
The live policy states things that are provably false against real behavior: "Allin does not actively share personal data with third parties" (wrong — Segment routes to Amplitude/Customer.io/Meta) and "we do not collect health data" (wrong — the check-in flow literally is special-category data collection). Every other gap on this page is invisible unless someone goes looking at code or a database; this one is a public page any user, journalist, or regulator can read today and see is false, which makes it the single most exposed item here. A corrected draft was written — and a few hours later, that draft was itself found saying check-in text is "never sent to any other outside provider," which stopped being true the moment Anthropic was discovered. Corrected openly rather than quietly edited.
Same Art. 28 requirement as the done processors above — these four just aren't resolved yet. Anthropic is the most urgent of the four specifically because it's the newest-confirmed processor, receiving real special-category text on every call — the least-covered flow gets the least excuse for an unsigned agreement. Customer.io is second because its DPA and terms are already fully known; it's purely an execution step away from closed. Meta and RevenueCat are lower urgency, each blocked by a distinct, named reason (a paused check, a missing login) rather than a generic "unconfirmed."
UK data controllers must register with the ICO, with narrow exemptions that don't apply to a company processing special-category health-adjacent data. Unlike most items on this page, this isn't a matter of interpretation — it's a binary yes/no, checkable in seconds on the public register, which is exactly why it's one of the more commonly enforced gaps in practice: an unregistered controller is trivially easy for anyone to spot.
Two distinct sources, not one: externally, the UK Children's Code applies to any service children are likely to reach, not just ones aimed at them, and paid Meta ads mean the real audience age isn't controlled. Separately, and just as real: Allin's own Terms already state a 16+ requirement with parental consent under 18, with zero enforcement behind it — the same class of accuracy problem as the Privacy Policy's false statements, just sitting in the Terms instead. Either source alone would justify action; having both is why this sits in Compulsory rather than Desirable. Recommended approach: self-declared birthdate at signup, hard-block under-16 — lowest friction that actually makes the existing Terms wording true.
Easy to conflate with the age-gate above, but a different authority and mechanism: Apple independently requires every app to carry an age rating based on a content questionnaire in App Store Connect, enforced at app review, separate from anything GDPR or the ICO requires. Allin now has real crisis-support content (Shout/Samaritans) and emotional-distress themes that weren't necessarily reflected in whatever rating was set at first submission.
Art. 32 names pseudonymization as an example security measure, not a mandatory one on its own — GDPR requires "appropriate" security, and Stage 1 plus the redacted views already reduce everyday risk. Stages 2/3 close the gap for a full-privilege login specifically, a real and serious exposure, but a risk-reduction choice on top of the legal floor rather than the floor itself — which is why it's Marie's call on timing, not an unconditional requirement. Marie's decision was to go all the way rather than stop at Stage 1, precisely because "reduces the risk" isn't the same as "removes it."
Also a named Art. 32 example, not a standalone mandate. Pseudonymization separates who from the content; encryption protects the content itself even if someone did get to it — either alone is meaningful but partial, together they deliver "even a full-privilege login learns nothing." The iOS integration is deliberately held back from this pass since it's the single most-used code path in the app (every check-in save and load) and deserves its own dedicated review.
Advertising standards and consumer protection are a genuinely different legal domain from data protection, but a public claim that overstates what Allin does is a real exposure of its own kind, worth tracking here rather than assuming "not GDPR" means "not relevant." Everything else on the claims register was checked and is defensible — these two specifically need Isabel's judgment on exact wording, not a legal determination.
emotional_sessions' free-text fields. beliefs_plans' intake text (life-area ratings + 4 free-text fields each) is the same sensitivity and isn't covered. Same architecture would extend cleanly — a later pass, not urgent.dormant_accounts() only identifies accounts past 180 days today — it doesn't warn, and it doesn't delete. A nice-to-have, not a requirement: an actual email warning at, say, 150 days, then auto-delete at 180 if no response. Turns a passive report into something that actually reduces stored data over time.