Allin — internal
Enter the password to continue.
Allin — Lifecycle Marketing Data Flow

Allin — lifecycle marketing data flow

A full trace of how a person's data actually moves through the product for email and push — identity, events, consent gates, and the two real breakages found along the way. Built to answer directly: did we land on the issue where emails or push tokens didn't reach Customer.io — and if so, exactly where. Checked against live code and the live database, not assumed.

The flow, at a glance

Appidentify() + track()
Consent gateclient-side only, unverified server-side
Segment SDKon-device
Segment Cloudroutes by destination
Amplitude
analytics — email stripped
Customer.io
lifecycle — email + push
APNsdevice token
registerDeviceToken()sets context.device.token
Racevs. auto "Application Opened"
Customer.iolikely never gets the token
Backendrecommend-plan, revenuecat-webhook
Consent gatebuilt, not deployed — fires unconditionally today
Segment Cloudsame routing as above

Green = confirmed working as intended. Amber = a real, evidenced gap. Detail on every box below.

Identity — can we even reach anyone?

Only 8 of 122 users have an email on file

Checked live against auth.users just now
Real constraint
6.5%of all users have any email captured at all — 8 out of 122, live count. Email is only captured for people who signed in after the Apple Sign-In email-scope fix; everyone who signed up before it has none.
This is the single biggest practical constraint on lifecycle email today, independent of whether the pipeline is wired correctly — even a perfectly-wired system can only email 6.5% of the base. It also directly undercuts the "send a consent opt-in email campaign" plan flagged on the CRM wiki: an email send can only ever reach the same 6.5%. If push also can't be relied on (see below), there's no external channel that reaches the other 93.5% — the only reliable path to them is inside the app itself, next time they open it.
See it
Live query against auth.users, 2026-08-20.

How identity actually reaches Customer.io

AnalyticsClient.identify(userId, email) is called from 5 places — initial sign-in, cached-session restore, sign-in success, and the point analytics consent is first granted. Every call sends one Segment identify(), which Segment fans out to every enabled destination from a single payload — there's no way for the app to send email to Customer.io but not Amplitude from one call. That's exactly why the Amplitude email fix was done as a Segment-dashboard mapping (a destination-level filter dropping the email trait for Amplitude specifically) rather than removing email from the app's call — confirmed by reading identify() directly, it still sends email unconditionally today, so Customer.io's copy of every identify call is untouched by that fix.
See it
Allin/Core/AnalyticsClient.swiftidentify() · called from AuthenticationManager.swift:40,55,232, AppView.swift:335, SettingsView.swift:448

Events that could trigger lifecycle marketing

Client-side — everything the app itself sends

MomentEvent(s)Source
Onboardingonboarding_started, onboarding_step_viewed, onboarding_completed, onboarding_skippedOnboardingFlow.swift
Sign-in / accountsign_in_succeeded, sign_in_failed, sign_out, account_deleted, consent_grantedAuthenticationManager.swift, AppView.swift
Check-incheckin_started, checkin_completedAppView.swift
Planplan_step_started, plan_step_completed, plan_block_completedAppView.swift
Audio sessionaudio_session_started, audio_session_completed, audio_session_paywalledAudioSession.swift
Paywall / subscriptionpaywall_viewed, paywall_continued_free, paywall_restore_tapped, paywall_dismissed, subscribe_clicked, subscribe_succeeded, subscribe_failed, subscribe_cancelledPaywallView.swift, SubscriptionManager.swift
Notifications / deep linksnotification_opened, deeplink_openedAppDelegate.swift, AppView.swift
Referralreferral_share_presentedAppView.swift

Every one of these is gated by the client-side consent check (below) before it fires — but see the audit-trail gap for why that gate's real-world coverage is uncertain.

Server-side — sent by the backend, outside any client gate

recommend-plan sends plan_block_generated plus a Customer.io identify() carrying real selected life-areas and 1–10 ratings, on every plan generation. revenuecat-webhook forwards every subscription lifecycle event (started, renewed, cancelled, billing issue, expired). Both run entirely server-side — triggered by an HTTP call or RevenueCat's own webhook — so the app's on-device consent flag never applies to them at all.
See it
allin-backend · supabase/functions/recommend-plan/index.ts · revenuecat-webhook/index.ts

Consent gating — client and server

Client-side gate exists — but nothing durable backs it up

Real gap
AnalyticsClient.hasAnalyticsConsent is checked before configure()/identify()/track() — that part works. The problem is what it's checking: a local UserDefaults flag, with no server-side record of whether that flag reflects a real, provable grant. Checked live just now: the user_consents table still doesn't exist in production, and record_consent_granted's live version still only writes a single blanket timestamp with no argument for which consent was actually chosen. So the gate that decides whether any of this fires is real, but unaudited — there's no way to demonstrate, for any given user, that their current device-local flag matches an actual choice they made.

Server-side gate — built, confirmed not deployed

Built, not live
A shared _shared/consent.ts helper is written and wired into both recommend-plan and revenuecat-webhook, fail-closed (no consent row, or a lookup error, resolves to "don't send"). Checked live just now: both functions are still at the exact same versions as before this was built (recommend-plan v33, revenuecat-webhook v5) — the gate exists on a branch, not in production. Every plan generation and every subscription event is still forwarded to Segment unconditionally today.
See it
allin-backend · branch fix/server-side-analytics-consent-gate

The push-token race — the likely real answer

Push device tokens almost certainly never reach Customer.io

A structural timing race, confirmed in the actual SDK source and Customer.io's own documented integration requirements
Real gap
Customer.io's Segment integration only reads a push token from context.device.token when it's already set on one of three specific events — Application Opened, Application Installed, or Application Uninstalled. Segment's SDK only sets that field once registeredForRemoteNotifications(deviceToken:) is called (confirmed directly in the analytics-swift source — it writes to context.device.token via a dedicated plugin).

In AllinApp.swift, AnalyticsClient.shared.configure() starts the SDK, which fires Application Opened automatically, essentially at launch. In AppDelegate.swift, requesting the push token requires a real network round-trip to Apple's servers before the token is known — structurally slower than a same-run-loop event. The two paths are independent, unsynchronized Tasks. On a cold launch, Application Opened almost certainly fires — and is sent to Customer.io — before the token exists to attach to it. This isn't a first-install-only problem: Application Opened fires on every cold launch.

Confirmed there's no fallback: only analytics-swift (Segment), purchases-ios-spm (RevenueCat), facebook-ios-sdk, and supabase-swift are linked in the project — no Customer.io native SDK exists in this app at all. The fragile Segment-context mechanism above is the only path a device token has to reach Customer.io, and it's the one racing.
Two real fixes, neither built yet
1) Add Customer.io's own iOS SDK (its Messaging Push module) and call its dedicated device-token method directly from AppDelegate — fully decoupled from Segment's timing, the path Customer.io's own docs recommend for this exact setup. Needs a new SPM package (a build change) and a new Push API Key from the Customer.io dashboard.
2) Stay Segment-only, and re-fire a signal once the token is known. Cheaper, but the safe version means re-sending the literal Application Opened event, which also reaches Amplitude by default and would double-count that event — analytics-swift 1.9.3 has no simple per-call destination filter, so this needs its own careful build to avoid corrupting an Amplitude metric while fixing a Customer.io one.
See it
Allin/Core/AnalyticsClient.swiftregisterDeviceToken · AppDelegate.swift · AllinApp.swift · Package.resolved (no CustomerIO SDK) · Segment/Customer.io's own documented integration requirements

What can't be verified from here

The actual Segment → Customer.io destination mapping

Marie — 2-minute check
Everything above about which event names trigger Customer.io's device-token or identify mapping comes from Customer.io's own public documentation, not a look at the actual configured mapping for the "Allin iOS" source — that lives in Segment's web dashboard, which needs a login this environment doesn't have (the same access gap as the missing SEGMENT_ACCESS_TOKEN, already known). Worth two minutes: Customer.io (Actions) destination → mappings → confirm what's actually wired for email and device.token, rather than relying on what's typical.

What Customer.io campaigns/journeys actually exist and fire

Marie — check in the Customer.io dashboard
This audit traces data into Customer.io — it can't see what's configured to happen once data arrives, since that needs Customer.io's own App API (a different credential from the Track API key already confirmed working in Supabase secrets), which isn't available here. Worth a direct look: which campaigns/journeys are live, what triggers each, and whether any assume a device token or email that (per above) may not actually be arriving reliably.

Bottom line

Priority order

1. Decide the push-token fix — native Customer.io SDK vs. a careful Segment-only workaround. Real, live gap affecting push for effectively every user today.
2. Reconsider the consent opt-in campaign's channel — an email send reaches at most 6.5% of users; push can't be relied on either per above. The only channel that reliably reaches the other ~93.5% is inside the app itself (a banner or modal on next open), not an external send.
3. Confirm the Segment → Customer.io mapping live — 2 minutes, closes the biggest remaining unknown in this whole trace.
4. Deploy what's already built — the server-side consent gate and the consent audit trail, both sitting on a branch, both confirmed still not live.
5. Check what campaigns actually exist in Customer.io directly, since this audit can trace the data in but not what happens to it once there.

Who does what

M

Marie

Decide the push-token fix approach (native SDK vs. workaround) · check the Segment → Customer.io destination mapping live · check what Customer.io campaigns/journeys actually exist and what they assume · decide the consent opt-in approach given the 6.5% email-reach constraint · approve deploying the server-side consent gate + consent audit trail