Skip to main content
Solved

Multiple anonymous aliases for my users


Forum|alt.badge.img

I need some serious help as I think I messed things up with my latest code update.  I use the Flutter SDK and this is what I do when the app starts up:

    configuration.purchasesAreCompletedBy = PurchasesAreCompletedByRevenueCat();
    if (authService.currentUser != null) {
      configuration.appUserID = authService.currentUser!.uid;
    }

    await Purchases.configure(configuration);
    if (authService.currentUser != null) {
      if (authService.currentUser!.displayName != null) {
        Purchases.setDisplayName(authService.currentUser!.displayName!);
      }
      if (authService.currentUser!.email != null) {
        Purchases.setEmail(authService.currentUser!.email!);
      }
    } else {
      log.t('4. User is not logged in, setting app user ID to null');
      configuration.appUserID = null;
    }

The authService just checks if the user is logged in and if they are, it assigns the actual uid from the app to the user account so that I can find them quickly.

However, I noticed with this code, my subscriber from last week who didn’t have their UID set as I had a bug in this code (I called Purchases.configure AFTER setting the displayName / email instead of before) had multiple aliases created this morning:

Last opened the app
2025-05-15 at 08:01 AM UTC
Created a new alias $RCAnonymousID:REDACTED9f11
2025-05-15 at 07:12 AM UTC
Created a new alias K<REDACTED CORRECT UID FROM APP>1
2025-05-15 at 07:12 AM UTC
Created a new alias $RCAnonymousID:REDACTED4ff29
2025-05-15 at 07:11 AM UTC

They now have three anonymous alias and the correct UID from the app.  This isn’t because they were using a different device - I can see it’s all from the same device.  Can anyone please help?

 

I had another user complain to say that their entitlement was taken away and I found one account with just their email address and another account with their email address and UID (the entitlement seemed to have worked until I did the update with the code as above).

I fear I may have broken things!!!

In fact, I just checked and noticed that one of my other subscribers had a new account created when they downloaded my new app version.  Can anyone spot issues here?

Best answer by guilherme

Hey ​@Mulder00 ,

Based on your logs and code, there seems to be three things happening:

  • multiple aliases created within seconds
  • a user lost their entitlement
  • a new user account was created after your latest app version

Looking at your initialization code:

configuration.purchasesAreCompletedBy = PurchasesAreCompletedByRevenueCat();
if (authService.currentUser != null) {
  configuration.appUserID = authService.currentUser!.uid;
}
await Purchases.configure(configuration);

Can you confirm:

  1. at what point in your app's lifecycle this code runs?
  2. are you certain that authService.currentUser is fully resolved at this stage? Meaning, this code always completes checking before proceeding?
  3. does this code ever run more than once (e.g., when app moves between background/foreground, or on auth state changes)?

The reason I ask is because the alias chain we're seeing:

07:11 AMCreated $RCAnonymousID:4ff29
07:12 AMCreated $RCAnonymousID:9f11
07:12 AMCreated K<actual UID>1

suggests that configure() might be running multiple times with different user states.

 

When configure() runs:

  • If appUserID isn't set, RevenueCat creates an anonymous ID ($RCAnonymousID:...)
  • If appUserID is set to a user's UID, that becomes their RevenueCat identity
  • If you later call configure() again or use logIn() with a different ID, RevenueCat creates an alias chain

A way forward is to configure RevenueCat once, after you're certain about the user's state:

final user = authService.currentUser; //assuming this resolves here and then

final config = PurchasesConfiguration(apiKey);
config.purchasesAreCompletedBy = PurchasesAreCompletedByRevenueCat();
config.appUserID = user?.uid;

await Purchases.configure(config); //only called once

if (user != null) {
  if (user.displayName != null) {
    await Purchases.setDisplayName(user.displayName!);
  }
  if (user.email != null) {
    await Purchases.setEmail(user.email!);
  }
}

and for followup logins, use logIn() instead of reconfiguring:

final result = await Purchases.logIn(user.uid);

More about identifying users here.

About your existing aliases, it seems you have your project's restore behavior as "Transfer to new App User ID", which is working as intended:

  • when aliases are created, entitlements transfer to the new ID;
  • all aliased IDs (anonymous and custom) are treated as the same customer;
  • users can always Restore Purchases to sync their entitlements (make sure a user does this themselves and not programmatically)

Note: appUserID = null set after configure() doesn’t change anything, as the SDK is already initialized by that point.

 

To help in your development, I'd suggest (in case you haven't), to enable debug logs to track user ID changes:

Purchases.setDebugLogsEnabled(true);

 

Best,

Gui

View original
Did this post help you find an answer to your question?
This post has been closed for comments

4 replies

guilherme
RevenueCat Staff
Forum|alt.badge.img+3
  • RevenueCat Staff
  • 48 replies
  • Answer
  • May 20, 2025

Hey ​@Mulder00 ,

Based on your logs and code, there seems to be three things happening:

  • multiple aliases created within seconds
  • a user lost their entitlement
  • a new user account was created after your latest app version

Looking at your initialization code:

configuration.purchasesAreCompletedBy = PurchasesAreCompletedByRevenueCat();
if (authService.currentUser != null) {
  configuration.appUserID = authService.currentUser!.uid;
}
await Purchases.configure(configuration);

Can you confirm:

  1. at what point in your app's lifecycle this code runs?
  2. are you certain that authService.currentUser is fully resolved at this stage? Meaning, this code always completes checking before proceeding?
  3. does this code ever run more than once (e.g., when app moves between background/foreground, or on auth state changes)?

The reason I ask is because the alias chain we're seeing:

07:11 AMCreated $RCAnonymousID:4ff29
07:12 AMCreated $RCAnonymousID:9f11
07:12 AMCreated K<actual UID>1

suggests that configure() might be running multiple times with different user states.

 

When configure() runs:

  • If appUserID isn't set, RevenueCat creates an anonymous ID ($RCAnonymousID:...)
  • If appUserID is set to a user's UID, that becomes their RevenueCat identity
  • If you later call configure() again or use logIn() with a different ID, RevenueCat creates an alias chain

A way forward is to configure RevenueCat once, after you're certain about the user's state:

final user = authService.currentUser; //assuming this resolves here and then

final config = PurchasesConfiguration(apiKey);
config.purchasesAreCompletedBy = PurchasesAreCompletedByRevenueCat();
config.appUserID = user?.uid;

await Purchases.configure(config); //only called once

if (user != null) {
  if (user.displayName != null) {
    await Purchases.setDisplayName(user.displayName!);
  }
  if (user.email != null) {
    await Purchases.setEmail(user.email!);
  }
}

and for followup logins, use logIn() instead of reconfiguring:

final result = await Purchases.logIn(user.uid);

More about identifying users here.

About your existing aliases, it seems you have your project's restore behavior as "Transfer to new App User ID", which is working as intended:

  • when aliases are created, entitlements transfer to the new ID;
  • all aliased IDs (anonymous and custom) are treated as the same customer;
  • users can always Restore Purchases to sync their entitlements (make sure a user does this themselves and not programmatically)

Note: appUserID = null set after configure() doesn’t change anything, as the SDK is already initialized by that point.

 

To help in your development, I'd suggest (in case you haven't), to enable debug logs to track user ID changes:

Purchases.setDebugLogsEnabled(true);

 

Best,

Gui


Forum|alt.badge.img
  • Author
  • New Member
  • 4 replies
  • May 20, 2025

Thank you - I have checked everything now and it’s setup as it should be.  I suspect I broke things as I had incorrectly implemented this in a previous version which I just had to mop up now.  I think.  This was my previous broken code:

 

configuration.purchasesAreCompletedBy = PurchasesAreCompletedByRevenueCat();
    await Purchases.configure(configuration);

    if (authService.currentUser != null) {
      log.t(
          '2. User is logged in, setting app user ID to ${authService.currentUser!.uid}');
      configuration.appUserID = authService.currentUser!.uid;

      if (authService.currentUser!.displayName != null) {
        Purchases.setDisplayName(authService.currentUser!.displayName!);
      }
      if (authService.currentUser!.email != null) {
        Purchases.setEmail(authService.currentUser!.email!);
      }
    } else {
      log.t('4. User is not logged in, setting app user ID to null');
      configuration.appUserID = null;
    }

As you can see, I ran configure first and then tried to set the appUserId.  It seems that anyone who opened the app with this version and then upgraded, got new accounts created instead of aliases.  (I’m posting it here in case someone else ever has the same issue).

AuthService is fully up and running before the RevenueCat is instantiated and it’s instantiated only once during start up.

In addition, I also have a listener set up to listen for auth state changes:

    _authListener = authService.onLogInLogOut.listen((user) async {
      if (user != null) {
        await Purchases.logIn(user.uid);
        if (user.displayName != null) {
          Purchases.setDisplayName(user.displayName!);
        }
        if (user.email != null) {
          Purchases.setEmail(user.email!);
        }
      } else {
        await Purchases.logOut();
      }

I’m assuming the way I have it set up now, aliases will be created instead of new users - it’s just anyone who used the previous version of my app with the buggy code, will have new accounts created instead of aliases?

EDITED TO ASK:  Should I use Purchases.logIn(uid) instead of configuration.appUserID = uid ???


guilherme
RevenueCat Staff
Forum|alt.badge.img+3
  • RevenueCat Staff
  • 48 replies
  • May 21, 2025

Hey ​@Mulder00 ,

Your current setup is much closer to best practice now, but just to clarify a few things (which I think are worth noting).

In your earlier implementation, the issue came down to the order of operations:

await Purchases.configure(configuration); configuration.appUserID = authService.currentUser!.uid; // too late

This is because when Purchases.configure() is called, the SDK reads the appUserID from the configuration object at that exact moment. If it's null or unset, the SDK initializes with an anonymous user.

Setting appUserID afterward has no effect, it doesn’t retroactively apply, and no error is thrown.

As a result, users who had previously been assigned an anonymous ID were seen as new users when the app updated with the fixed code. Since there was no aliasing between the anonymous and known IDs, the SDK created a new account, leading to the duplicate user entries you're seeing.

As to your new update, you're now doing two things correctly:

  • initializing RevenueCat only once, during app startup;

  • using an auth listener to call Purchases.logIn() or Purchases.logOut() in response to login state changes.

This is the recommended approach!

When a user logs in after being anonymous, logIn() creates an alias between the two IDs, ensuring entitlements and purchase history are preserved.

As to the appUserID vs logIn() approach, to explain a bit the difference/behavior:

  • Use appUserID in the PurchasesConfiguration object only before calling configure(), when the user’s ID is already known. If you have it before calling configure() this is okay to do;

  • For any login state changes after SDK initialization, use Purchases.logIn() and logOut().

In Flutter, and as we exemplify in Identifying Customers it’s common to use the cascade operator (..) to perform a sequence of operations on the same object, without having to repeat the object’s name. This is why this works, during the configure() call:

await Purchases.configure(
    PurchasesConfiguration('your_public_sdk_key')
      ..appUserID = authService.currentUser!.uid
);

It ensures the appUserID is set at the correct moment, before the SDK initializes.

 

As for users who launched the buggy version, they likely ended up with two separate accounts  (as we investigated, with anonymous and UID), because aliasing didn't occur. Your current setup fixes that, and going forward, new logins will be correctly aliased.

If you have users missing entitlements due to this, it’s worth prompting them to use the Restore Purchases feature in your app. This will help RevenueCat associate the correct entitlements with the current user ID. 

NOTE: It’s best to encourage users to use the “Restore Purchases” feature themselves, rather than triggering it programmatically in your app. Typically, this is presented as a button in your app’s settings or purchase screen, something like “Restore Purchases” or “Restore Subscriptions.”

This approach gives users control and clarity, especially if they’ve reinstalled the app, switched devices, or are troubleshooting access to their purchases. It also helps avoid confusion or unexpected changes to their account.

You should only trigger a restore programmatically if you’re absolutely certain it’s needed (for example, during a specific migration flow or if you have a very controlled use case).

For most apps, letting the user initiate the restore is the best practice.

 

Best,
Gui


Forum|alt.badge.img
  • Author
  • New Member
  • 4 replies
  • May 21, 2025

Thank you for your helpful and comprehensive responses, I appreciate it!  And for what it’s worth, I don’t even have restore purchases in my code - I use the Paywall feature which has a Restore Purchases button, so if anyone contacts me, I’ll let them use that in the first instance to restore their purchases.

Despite these teething issues, I have no idea how I would have ever implemented subscriptions in my app without RevenueCat - I’m sure you can imagine what a mess I would have made if I had to do this separately for iOS and Android 😆.


Cookie policy

We use cookies to enhance and personalize your experience. If you accept you agree to our full cookie policy. Learn more about our cookies.

 
Cookie settings