The report
A French user wrote in (via the in-app feedback, twice, minutes after purchasing) saying they had taken the annual subscription at “8 € per month” and still had no access to voice. They asked us to unblock it. The annual launch offer is 99.99/yr ≈ 8.33/month, so the plan matches.
Voice dictation is gated server-side on Autumn feature flags. Their Autumn customer has none. So from the app’s point of view they are a free user who is trying to use a Pro feature — and the paywall they land on tells them their subscription is “still syncing”, forever.
What happened
Every step below was checked against live Autumn and RevenueCat records and our source, then re-derived independently by three verification passes. Chips mark what is verified versus inferred.
-
The purchase is real verifiedRevenueCat holds one App Store subscription for the reporter’s user ID: annual launch offer, 7-day trial,
gives_access: true, entitlementproactive. Started 2026‑08‑28 03:47:08Z, country FR.RC v2GET /customers/{id}/subscriptions -
…but it was recorded under an anonymous ID verifiedThe subscription’s
original_customer_idis$RCAnonymousID:524aac…; itscustomer_idis the real user, because RevenueCat merged the two afterwards. Five control purchasers (iOS and Android) haveoriginal_customer_id == customer_id.Same RC record queried via the anonymous ID returns the same subscription. -
Autumn keyed on the anonymous ID and created an orphan customer verified13 seconds after the purchase, Autumn live gained a customer whose only identity is
processors.revenuecat.id = $RCAnonymousID:524aac…(Autumnid: null, no email) holdingpro_annual_launch_offeractive plus all three Pro flags and 500 messages. Autumn’s own API refuses to address it:customer_id: cannot contain '$'.Autumncustomers.list, created 03:47:21.959Z. Autumn’s open-source resolver keys on the webhook’sapp_user_id, thenoriginal_app_user_id, then auto-creates; it never readsaliasesand has no TRANSFER handler. -
The real customer stayed on Free verifiedAutumn customer
LjByEn2uCYLZ3ntfTfuXMAdQnniwkMBj: planfreeonly,processorsnull,flags {}, no invoices, no trials used. -
So the server denies voice — correctly verifiedFeature access is
customer.flags[featureId] !== undefined(packages/billing/src/server/billing.ts:89‑97);findDeniedFeaturereturnsvoice_transcription; the transcription route throwsPAYMENT_REQUIRED(packages/api/src/routers/transcription/transcription.router.ts:29‑36).Ruled out: monetization gating (can only fail open), TestFlight/sandbox routing (no such customer in sandbox), stale native cache (denial is server-side), a second account (only one Autumn customer for this person).
Affected users
All six orphans in Autumn live follow the same shape. All are App Store purchases; none are Android or web. Emails are masked here; IDs are the Better Auth / Autumn / RevenueCat user IDs and are enough to act on.
| Anonymous RC ID (orphan) | Real user ID | Country | Plan | Started (UTC) | Trial ends / renews | Autumn state | |
|---|---|---|---|---|---|---|---|
| $RCAnonymousID:8acc3d03… | Au0Fozjr8TEqykwT1l2tlUHCczq1tXGt | g•••@privaterelay.appleid.com | SA | Annual (trial) | 25 Aug 22:29 | 1 Sep | free |
| $RCAnonymousID:eefeaa85… | IE8lnmlJoNMEDzAOG4sgQB4WyIiruK7K | y•••@gmail.com | NG | Annual (trial) | 27 Aug 09:29 | 3 Sep | free |
| $RCAnonymousID:0cf9d45b… | vVvNvi0yAEuzjveOOOK3di7zACFRu4ja | s•••@gmail.com | SA | Annual (trial) | 27 Aug 09:59 | 3 Sep | free |
| $RCAnonymousID:b0e28f82… | lI5DMzt1vzCogi4CWjuVvPeyH9HoRpAC | s•••@icloud.com | SA | Monthly — paying | 27 Aug 10:47 | 27 Sep | free |
| $RCAnonymousID:83a39195… | e1cQZ1hUal1xE3jtDOK3lkzc2p7kcLKY | y•••@privaterelay.appleid.com | AE | Annual (trial) | 27 Aug 13:04 | 3 Sep | free |
| $RCAnonymousID:524aac16… | LjByEn2uCYLZ3ntfTfuXMAdQnniwkMBj | r•••@gmail.com reporter | FR | Annual (trial) | 28 Aug 03:47 | 4 Sep | free |
Population: 125 RevenueCat-backed Autumn customers; ~131 native paid subscriptions 24–28 Aug. Detection query: Autumn customers.list with processors: ["revenuecat"] and look for ids starting $RCAnonymousID.
Why the ID was anonymous
The outcome chain above is settled. The trigger — why RevenueCat had an anonymous identity at purchase time — is where the evidence is indirect, and where the investigation changed its mind. Two candidates, ranked.
RevenueCat’s Apple server-to-server notification race with non-UUID app-user IDs
RevenueCat’s documentation states that new purchases tracked from Apple’s server notifications are matched by the transaction’s appAccountToken, and that this token is not set when the App User ID is not a valid UUID, or when Apple’s notification reaches RevenueCat before the SDK’s receipt post. In that case:
RevenueCat will generate an anonymous App User ID to associate that purchase with. We will then proceed with your transfer behavior for the new App User ID set by the SDK.RevenueCat docs — Apple App Store Server Notifications → Tracking new purchases → User identity
Our app-user IDs are Better Auth’s 32-character IDs, not UUIDs, so the token is never set. Whenever Apple’s notification wins the race (slower mobile networks — the six are FR, SA ×3, NG, AE), RevenueCat creates an anonymous customer, fires INITIAL_PURCHASE for it, and later transfers the subscription to the real ID without any alias webhook. This explains everything that is otherwise odd: iOS only (Google’s RTDN path differs), a random ~5%, no auth churn, purchases logged under the real user in analytics, the 13-second gap.
Decisive check (needs RevenueCat dashboard): Apps → iOS → “Track new purchases from server-to-server notifications”, and the anonymous customer’s event history. Our API key lacks project_configuration:apps:read, so this could not be read programmatically.
The app anonymises the SDK on transient session loss
NativeBillingProvider receives userId = undefined whenever the Better Auth session is null or anonymous (apps/native/src/app/_layout.tsx:164); the adapter then calls Purchases.logOut() (revenuecat-adapter.ts:483‑497), which mints a new $RCAnonymousID. Better Auth nulls the session on a 401 during revalidation. This path is real and is the only anonymising call in the app — but purchases are serialised behind logIn/configure, the billing route requires a real user, and analytics show zero auth events for any of the six on their purchase day. Only an out-of-band StoreKit post (a pending/SCA transaction delivered later) could land in that window. Worth removing as hygiene; unlikely to be the cause.
Ruled out: an explicit sign-out or anonymous flow before purchase (no such events for any of the six); an older build configuring RevenueCat without an app-user ID (every RC-enabled build passes appUserID).
Latent second defect
After RevenueCat merges the anonymous ID into the real one, later events carry app_user_id = real and original_app_user_id = anonymous. Autumn’s resolver then matches both the orphan (via the processor ID) and the real customer (via customer.id) and throws 409 MultipleCustomersFound. Renewal, cancellation and expiration for these six will fail to sync to either customer: the orphan stays Pro forever, the real user stays Free forever. The first trial converts on 1 September.
This is why the orphans must be cleaned up, not merely worked around.
What users see
Tapping the microphone opens the billing screen in its subscription‑syncing state (“Your subscription details are still syncing” with a “Check again” button), because the RevenueCat SDK says entitled while summary.activePlanId is null. Settings show “Free Plan”. Restore cannot help — there is nothing to restore. That matches the reporter opening feedback twice within the hour.
Fixing the 6 accounts
1 · Autumn support safest
The RevenueCat integration is beta and the orphans are unaddressable by API. Ask Autumn, per user, to move the RevenueCat-provisioned product from the $RCAnonymousID orphan to the real customer, set the real customer’s processors.revenuecat to the real ID with the anonymous ID as an alias, and delete the orphan. This is the only path that also prevents the 409s on renewal.
Side effects: none on Stripe; possibly one billing.updated → welcome email from our webhook.
2 · Self-serve bridge sandbox-test first
Grant the real customer directly with no invoice — the same call Autumn’s own RevenueCat handler makes:
autumn.billing.attach({
customerId: "LjByEn2uCYLZ3ntfTfuXMAdQnniwkMBj",
planId: "pro_annual_launch_offer", // "pro" for lI5DMzt1…
noBillingChanges: true, // no Stripe writes
enablePlanImmediately: true,
planSchedule: "immediate",
redirectMode: "never",
endsAt: 1788493628000, // RC current_period_ends_at
});
Side effects: one welcome email. Nothing auto-renews or expires this — a re-check at trial end is required. Open risk: the public route may reject a priced plan for a customer with no Stripe customer.
- Retry/resend the RevenueCat webhook — it replays the same anonymous payload into the same orphan.
- Rename the orphan with
customers.update— the target ID already exists, and the$ID is rejected anyway. - Delete any orphan before the real customer has access.
- Attach via web checkout — that creates a Stripe subscription and invoice.
Code changes
Native — one PR, pure JS, no native rebuild
- Declare the canonical identity to Autumn. After
configure/logInsucceed (revenuecat-adapter.ts~L465–470), callPurchases.setAttributes({ autumn_customer_id: userId, autumn_customer_email: email }). Autumn resolves this subscriber attribute beforeapp_user_id(getRevenueCatOverrideCustomerId.ts, onmainsince 4 July). This single change neutralises every trigger candidate, including the Apple race. - Never anonymise the SDK. Remove the
Purchases.logOut()branch indisconnect(); keep the generation bump that already invalidates stale work. The nextconnect(otherUser)doeslogIn. - Bind before money moves. Immediately before
purchasePackageandrestorePurchases, assertgetAppUserID() === userId; re-logInonce if drifted, otherwise refuse. - Tests in
apps/native/test/billing/revenuecat-adapter.test.ts: nologOuton disconnect; purchase re-logs-in when the SDK ID drifted; purchase refuses when the SDK stays anonymous;setAttributescalled after connect.
Server — detection first, self-heal behind a flag
- Detect divergence. Extend the RevenueCat evidence schema (
adapters/revenuecat.ts) withcustomer_id,original_customer_id,ownership; inloadSubscriptionContext(billing.ts:139‑161) flag “RC says active, purchased, for this user ID — Autumn says free, no RC processor”. Log to Sentry (deduped via Redis), and expose it in the summary so the paywall can say something honest instead of “syncing” forever. - Self-heal (flagged). When divergence is detected from server-fetched RevenueCat evidence only, perform the attach above with
endsAtfrom RevenueCat, idempotent via a Redis lock and a metadata marker; extend or cancel on later checks. - Monitor. Daily job listing Autumn customers whose RevenueCat processor ID starts with
$RCAnonymousIDor differs from the customer ID; alert on RevenueCat webhook delivery failures (409s). - Get a RevenueCat key with
customer_information:customers:readfor audit tooling; ours cannot read aliases or history.
Vendors
- RevenueCat: if the Apple S2S trigger is confirmed, either disable “Track new purchases from server-to-server notifications” for iOS, or (larger change) move to UUID app-user IDs. The
autumn_customer_idattribute makes this optional. - Autumn: confirm production runs the July resolver (attribute override), ask them to consult
aliaseswhen anapp_user_idmatch exists and to handleTRANSFER; file the repro.
Decisions needed
- Who checks the RevenueCat dashboard for the S2S toggle and the anonymous customer’s history (confirms or kills the leading trigger).
- Remediation path for the six: Autumn support (slower, complete) vs. the self-serve attach (fast, needs a follow-up at trial end). Recommendation: open the support ticket now and sandbox-test the attach so we can unblock users if support is slow.
- Approve the native PR (attribute + no-logOut + identity assertion) for the next OTA/store build.
- Whether to ship the server self-heal or stop at detection + honest UI copy.
Reply to the reporter
Draft, French. Tells them the subscription is real, the fault is ours, nothing to re-buy, no timing promise.
Bonjour Rochdi,
Merci pour votre message et désolé pour la gêne. Nous avons vérifié : votre abonnement Qaf Pro est bien actif côté App Store et vous n'avez rien fait de travers. Le problème vient de chez nous : l'abonnement a été rattaché à un identifiant technique différent de votre compte, donc l'application ne vous reconnaît pas encore comme abonné (d'où le blocage de la dictée vocale).
Nous corrigeons cela sur votre compte ; vous n'avez rien à faire ni à racheter. Nous vous préviendrons par e‑mail dès que c'est en place — il suffira alors de fermer et rouvrir l'application.
Merci pour votre patience et pour nous l'avoir signalé.
Method & sources
Everything was read-only: no records were modified in Autumn, RevenueCat, Stripe, or the database.
- Autumn live API —
customers.list(search, processor and plan filters),customers.get; sandbox checked for the reporter (absent). - RevenueCat v2 API —
/customers/{id}/subscriptionsfor the six real IDs, the six anonymous IDs, and five control purchasers. - Mixpanel — per-user event timelines (project timezone America/Los_Angeles) for all six users, 24–28 Aug.
- Source —
packages/billing,packages/apitranscription/billing routers,apps/native/src/billing/*, Better Auth client internals, Autumn’s open-source server (RevenueCat resolver and webhook router), RevenueCat documentation. - Verification — an 8-agent pass: three adversarial verifiers with different lenses (data, mechanism, alternatives), two researchers (RevenueCat, Autumn), two code auditors (native identity lifecycle, server reconciliation), one synthesis. Verifiers upheld the outcome chain 3/3 and rejected the original “logOut on session loss” trigger 2/3 in favour of the Apple S2S race.
Key raw fields
RC subscription subAapc1e2e02aec111563ef5aa55a74a25f72
customer_id LjByEn2uCYLZ3ntfTfuXMAdQnniwkMBj
original_customer_id $RCAnonymousID:524aac169e10489b91cad72fc2c5ce4f
store app_store · country FR · status trialing · gives_access true
starts_at 2026-08-28T03:47:08Z · current_period_ends_at 2026-09-04T03:47:08Z
entitlement pro (active) · product prodae88158eaf · ownership purchased
Autumn live orphan (customers.list)
id null · email "$RCAnonymousID:524aac…" · processors.revenuecat.id "$RCAnonymousID:524aac…"
created_at 2026-08-28T03:47:21.959Z
subscriptions: cus_prod_3IWmkaKGMzdfmHL89RrLDoHEJVV pro_annual_launch_offer active (trial ends 2026-09-04)
flags: voice_transcription, search_filters, image_uploads
Autumn live real customer LjByEn2uCYLZ3ntfTfuXMAdQnniwkMBj
created_at 2026-08-23T00:21:33Z · plan free · processors null · flags {} · invoices []