Everything touching Segment → Amplitude / Customer.io / Meta, plus RevenueCat and Anthropic as data processors — re-checked tonight against the actual live code, not the intended design or the previous version of this page. Three previously-undocumented gaps turned up that way, two of them already fixed. Sorted by how much each actually matters, not by when it was found.
git branch -a plus a diff against main on both repos (iOS branch fix/apple-signin-email-scope; the backend has 7 open branches touching consent, erasure, export, and pseudonymization), and a direct read of every consent-adjacent surface — Allin/Home/SettingsView.swift, ConsentSheet.swift, AnalyticsClient.swift, MetaAdsClient.swift, and every backend function that talks to Segment (recommend-plan, revenuecat-webhook, delete-user-data). One claim — "Meta advertising is properly gated" — got checked against the actual linked FBSDKCoreKit SDK source (a real local checkout under Xcode's DerivedData, not the docs or this app's own comment asserting it), because an unverified claim about consent gating is exactly the kind of thing that's easy to assert once and never check again. That check is what surfaced the finding below. Three real, previously-undocumented things came out of this pass — two already fixed, one already resolved on a branch: (1) two backend functions were forwarding real analytics events — for one of them, the user's actual check-in areas and 1–10 ratings — to Segment with zero consent check, because they run entirely server-side, outside the app's own consent gate; (2) Meta was receiving real purchase and registration events regardless of advertising consent, because the setting this app's own code (and this page) claimed gated that only gates Meta's automatic events, not the explicit ones actually being sent; (3) the recommend-plan repo/live drift flagged last time as an active, unresolved risk has, since, actually been fixed and committed — it just hasn't been pushed. Terms like "edge function," "migration," "committed vs. deployed" are the same vocabulary as the GDPR page's glossary — not repeated here.delete-user-data's own code comments rather than assumed: Segment's user-deletion/suppression API ("Regulations," the one that would actually erase a person's record inside Segment itself) requires a Team/Business-tier workspace, not Free. So even once the missing SEGMENT_ACCESS_TOKEN/SEGMENT_SOURCE_ID are resolved, Segment-side erasure may still not work on the current plan — worth knowing before spending more effort chasing the token alone. Not urgent: Amplitude and Customer.io erasure already work independently of this (see the medium-priority item below).user_entitlements table live, linked to RevenueCat via revenuecat_app_user_id, updated by the RevenueCat webhook — as before. Newly confirmed by reading the actual webhook code (not assumed from the table existing): it also forwards every subscription lifecycle event — started, renewed, cancelled, billing issue, expired — to Segment → Amplitude/Customer.io itself, server-side, on RevenueCat's own schedule. That's a separate, direct integration from anything the iOS app's AnalyticsClient sends from the device — see the consent-gate finding below for why that distinction mattered.classify-affect, classify-crisis, recommend-plan) — only check-in/intake free text goes across, nothing identifying.recommend-plan forwards a plan_block_generated Segment event and a Customer.io identify() call carrying the user's real selected areas and 1–10 ratings — literally which belief areas they're least satisfied with — every time a plan is generated. revenuecat-webhook forwards every subscription lifecycle event the same way. Both fire completely unconditionally: no check against the user's analytics-consent choice, ever, at either site.AnalyticsClient's consent gate (hasAnalyticsConsent, checked before .configure()/.identify()/.track()) only covers events the iOS app decides to send from the device. Both of these run server-side — one triggered by an HTTP call, one by RevenueCat's own webhook — so there was never a client-side check to bypass; nothing stood in for it on the server either._shared/consent.ts helper that looks up the most recent analytics row in user_consents (the same audit-trail table from the consent-RPC fix below) for a given user id, and gates both call sites on it. Fails closed on purpose: no row on record, the migration not deployed yet, or a lookup error all resolve to "don't send" — matching what GDPR actually requires (affirmative consent, not silence read as yes).fix/server-side-analytics-consent-gate (merges combined-fixes-2026-08-15 + feat/gdpr-consent-record). Depends on the user_consents table actually being live — until then this just means the events stop firing, not that anything errors.user_consents (it didn't exist until this table shipped), so the moment this gate deploys, subscription/plan analytics for every current user goes quiet in Amplitude/Customer.io until they revisit the consent screen or Settings → Privacy. Decided: no silent backfill. Instead, a specific push/email campaign will ask existing users to actively opt in — see the new to-do card just below. That's the GDPR-cleaner choice (real, provable consent going forward, not an assumed "yes" for people who were never shown the new granular screen), at the cost of a real gap in Amplitude/Customer.io continuity for anyone who doesn't respond to the campaign.user_consents table actually being live first — sending it before that deploys would ask people to opt into something that isn't wired up yet.user_consents + the server-side gate first, then this campaign, ideally before or alongside the same release that ships the gate — otherwise the "gone quiet" window is longer than it needs to be.context.device.token when it's already set on one of three specific events — Application Opened, Application Installed, Application Uninstalled — per Customer.io's own integration docs. Segment's iOS SDK only sets that field once registeredForRemoteNotifications(deviceToken:) is called, which is what AnalyticsClient.registerDeviceToken() does.AllinApp.swift, AnalyticsClient.shared.configure() starts the Segment SDK — which fires Application Opened automatically, essentially at launch, via trackApplicationLifecycleEvents(true). In AppDelegate.swift, registerForRemoteNotifications() requires a network round-trip to Apple's APNs servers before didRegisterForRemoteNotificationsWithDeviceToken fires and the token becomes known — structurally slower than a same-run-loop-turn event. The two paths are independent Tasks with no ordering between them. On a cold launch, the odds strongly favor Application Opened firing — and being sent to Customer.io — before the token exists to attach to it. This isn't first-install-only: it's every cold launch, since Application Opened fires on each one.Package.resolved and the Xcode project directly — only analytics-swift (Segment), purchases-ios-spm (RevenueCat), facebook-ios-sdk, and supabase-swift are linked. No Customer.io native SDK exists in this app at all, so the Segment-context mechanism above is the only path a device token has to reach Customer.io — and it's the one racing.AppDelegate — this is what Customer.io's own docs recommend for exactly this setup. Fully decoupled from Segment's timing: the token reaches Customer.io the moment APNs returns it, launch-lifecycle-event timing stops mattering. Cost: a new SPM package added in Xcode (a build change — needs you, not me), a new "Push API Key" from the Customer.io dashboard (different credential from the Track API key already in Supabase), and one real test push to confirm delivery.Application Opened event name (since that's what Customer.io's mapping is documented to trigger on), which also reaches Amplitude by default and would double-count that event for the session. analytics-swift 1.9.3 doesn't expose a simple per-call "send to this destination only" flag; there's an enrichment-closure mechanism that could suppress it for Amplitude specifically, but I haven't built or tested that, and getting it wrong quietly corrupts an Amplitude funnel metric — the same class of mistake this whole page exists to catch.SEGMENT_ACCESS_TOKEN, already known). Customer.io (Actions) destination → mappings → confirm what's actually configured, rather than relying on Customer.io's public docs for what's typical.registerDeviceToken, configure) · Allin/Core/AppDelegate.swift · Allin/Core/AllinApp.swift · Allin.xcodeproj/project.pbxproj + Package.resolved (no CustomerIO SDK linked)AnalyticsClient.identify() itself, which still sends userId + email in one shared call feeding every destination. Customer.io's copy of that same call is untouched by that fix and should be unaffected — that's as far as this can be confirmed from the code. Whether Customer.io's own destination mapping is actually configured to consume the email trait is a Segment-dashboard setting, same access gap as above. If a specific past incident is what you're remembering, it's worth two minutes in Segment: Customer.io (Actions) destination → Identify mapping → confirm email maps to Customer.io's email field, then check one real test user actually shows up as a person in Customer.io with that email attached.MetaAdsClient's enable/disableTrackingSettings() toggle two SDK settings (isAutoLogAppEventsEnabled, isAdvertiserIDCollectionEnabled) based on the user's advertising-consent choice — that part was real and correct. But trackPurchase() and trackCompleteRegistration() — called unconditionally, no consent check at either call site (SubscriptionManager.swift on every successful subscribe, AuthenticationManager.swift on every Apple sign-in) — call Meta's SDK through a different path (AppEvents.shared.logPurchase / .logEvent) that those two settings don't actually gate at all.isAutoLogAppEventsEnabled only gates logInternalEvent — the SDK's own automatic events. The explicit logEvent/logPurchase path (doLogEvent:) has no consent-style check in it at all, only a remote kill-switch. So a user who explicitly declined advertising consent was still generating a real Purchase event — actual amount and currency — and a CompleteRegistration event inside Meta's system.MetaAdsClient itself — one hasConsent gate reused by track, trackPurchase, trackStartTrial, and trackCompleteRegistration — instead of relying on every call site to remember to check first, since that's the exact pattern that had already failed twice.fix/apple-signin-email-scope (not yet pushed). No visual or copy change — this only changes which network calls fire.record_consent_granted does already exist live — but it's an older, zero-argument version (the one your own project notes already flagged as untracked-but-live), and all it does is stamp a single blanket timestamp into user_entitlements.consent_granted_at — no record of which specific consents (analytics/advertising/reminders) were actually granted. The app's real call — on main, live today — doesn't call this function at all; the onboarding consent sheet only sets a local on-device flag, nothing reaches Supabase. The 3-argument version of the call (p_analytics/p_advertising/p_reminders) only exists on this session's branch, wrapped in a silent try? — and because no live function matches those 3 named parameters, that call would also fail silently if shipped as-is today. Live proof, checked directly: 0 of 112 real users have consent_granted_at set at all. Nothing about consent has ever actually landed in Supabase for anyone.user_consents table (one row per grant/withdrawal: user id, which consent, on/off, timestamp) plus a new, correctly-3-argument record_consent_granted (matching what the app actually calls) and record_consent_change (a second function for single-toggle changes). The onboarding side was wired first; Settings → Privacy's two toggles were wired to record_consent_change this session — before that, changing your mind after the fact wasn't recorded at all, only the very first choice was (once the RPC itself existed).feat/gdpr-consent-record. iOS wired on both entry points — branch fix/apple-signin-email-scope. Neither is deployed/pushed — re-verified directly against live Supabase just now: no user_consents table, no record_consent_change function exist live yet. Until this migration deploys, consent recording stays exactly where it is today: nowhere.recommend-plan tracked on main was FTS-only (no Anthropic call) while the function actually running in production used a Claude/Anthropic composition path — meaning the next unrelated push to main would have silently redeployed the stale version over the correct live one.supabase functions download, not guessed from a binary bundle) and every function now matches what's really running — recommend-plan, plus three previously-untracked functions (recommend-audio, classify-affect, classify-crisis) that existed live but nowhere in the repo. CI also changed so each function only redeploys when its own folder changes, closing the exact mechanism that made this dangerous.fix/recommend-plan-repo-drift (also present in combined-fixes-2026-08-15). What's left isn't technical — it's approval to push, since main today still doesn't reflect any of it.delete_user()) is written, committed, and confirmed to have the secrets it needs already live. It's just never been deployed.fix/apple-signin-email-scope branch's deleteAccount() already calls delete-user-data's URL instead of the raw delete_user RPC. The live App Store build still correctly calls the raw RPC (confirmed separately on main), so nothing is broken today — but it means delete-user-data has to be deployed before or in the same release as that iOS branch ships. If the iOS update goes out first, every account deletion hits a 404 against a function that doesn't exist yet.user_entitlements already works independently of this.We want to make this practice better,
your choice, for each of these.
record_consent_granted. Source: Allin/Home/ConsentSheet.swiftprivacy & data
record_consent_change. Source: Allin/Home/SettingsView.swift — PrivacyConsentViewCross-references: the fuller GDPR picture (pseudonymization, encryption, consent copy sign-off, the new "download my data" Settings entry point) lives on the GDPR & onboarding wiki. The DPA checklist itself is ~/Documents/allin-gdpr/DPA_CHECKLIST.md.
One CRM-specific item below has a ready prompt — paste it into your own Claude. The rest of your open items touching this workstream (consent-screen sign-off, the age-gate call, marketing claims) already live on the GDPR page's own "who does what" — not duplicated here.
user_consents migration together with the new server-side consent gate (one doesn't do much without the other live) · decide the pre-existing-user backfill question for that gate, with Isabel · regenerate the RevenueCat v2 API key · re-check Segment's plan tier before investing more in the missing access token, given the Team/Business-tier requirement found tonight.