Skip to content
SaaS Development6 October 2026 · 11 min read

Keep RevenueCat Access Correct When Users Switch

Signing out of Firebase does not change who RevenueCat answers for. The identity calls, the one that throws when nobody is logged in, and where entitlements get enforced.

Keep RevenueCat Access Correct When Users Switch

A paid account signs out on a shared phone. The next person signs in, reaches the home screen, and every premium feature is still open to them. No purchase was made on that account, no receipt exists for it, and the app is completely confident about the access it just granted.

Signing out of Firebase Authentication does not change who RevenueCat thinks it is. These are two identity systems with two separate switches, and the SDK keeps answering for the previous App User ID until something tells it to stop. The repair is to pair every Firebase sign-in and sign-out with a RevenueCat identity call, guard the one that throws, and treat "not known yet" as a real state your UI can render.

This article covers the transition between identities. If you are still building the purchase flow itself, gating a Flutter app behind a RevenueCat paywall handles the single-user case; this one starts at the moment a second account appears on the same device.

Why the next account still sees premium

RevenueCat identifies a customer by an App User ID and keeps using that identifier until your code changes it. If you never supplied one, the SDK invented one: anonymous identifiers are prefixed with $RCAnonymousID: and cached on the device, and a reinstall clears that cache and produces a new one.

So a single device holds two independent answers to the question of who is using it:

  • Firebase Authentication answers with the signed-in user. Its authStateChanges() stream emits right after the listener is registered, when a user signs in, and when the current user signs out.
  • RevenueCat answers with the current App User ID, which changes only when you call logIn or logOut.

Sign out of Firebase alone and only the first answer changes. The second still names the account that paid, which is why the next person inherits it.

Pick one identifier and never change your mind

Use one stable, non-guessable identifier for the lifetime of the account, supplied by the system that already owns identity. In a Firebase app that is the uid.

RevenueCat's guidance on choosing an App User ID is specific: unique per user, under 100 characters, and ideally a non-guessable pseudo-random value such as a UUID. It is equally specific about what to avoid.

CandidateWhat the documentation saysWhat it costs you
Firebase Auth uidNot named in the docs, but it satisfies the stated constraints: unique, opaque, stable, shortNothing, provided you use it everywhere and never fall back to another value
Email address"we don't recommend using email addresses as App User IDs"Changes when the person changes it, and leaks an identifier you did not mean to publish
Device or advertising identifier"Advertising identifiers should not be used"Identifies a device rather than a person, so the purchase stays with the phone
A hardcoded string"Don't hardcode strings as App User IDs"Every install collapses into one customer

The first row is a recommendation rather than a documented instruction. RevenueCat does not name Firebase; the uid simply meets the constraints it does state, and your backend already trusts it.

Change the identity at the two moments that matter

Two parallel tracks crossing a sign-out and a sign-in boundary: the Firebase Authentication track reads user A, then signed out, then user B, while the RevenueCat App User ID track reads uid_A, then an anonymous $RCAnonymousID: value, then uid_B, moved by Purchases.logOut() and Purchases.logIn(uid).

Configure the SDK once, then move the identity on sign-in and on sign-out. There is no third moment.

  1. Configure once at startup, before any purchase code runs. RevenueCat's SDK configuration guide says to configure only once, early in the application lifecycle. If nobody is signed in yet, pass no identifier and let the SDK generate an anonymous one.
  2. On sign-in, call logIn with the uid. It returns a LogInResult carrying the fresh customerInfo for that identity and a created flag that is true the first time RevenueCat has seen this App User ID.
  3. On sign-out, call logOut before you sign out of Firebase. You still have a confirmed identity at that point, and a failure leaves the person signed in rather than signed out with someone else's purchase identity attached.
// Reference implementation. Not executed against a store sandbox.
class SessionIdentity {
  Future<CustomerInfo> onSignedIn(String uid) async {
    final LogInResult result = await Purchases.logIn(uid);
    // result.created is true the first time RevenueCat sees this App User ID.
    return result.customerInfo;
  }

  Future<void> onSignOutRequested() async {
    // logOut throws when the current user is already anonymous, so ask first.
    if (!await Purchases.isAnonymous) {
      await Purchases.logOut();
    }
    await FirebaseAuth.instance.signOut();
  }
}

Step three is ordering advice rather than a documented requirement. The documented parts are the method signatures and what each one does.

What logOut does when nobody was logged in

It throws. The Flutter API reference for logOut states that it throws a PlatformException if there was a problem restoring transactions, or if the method is called while the current user is anonymous. On success it clears the saved App User ID and generates a new anonymous one.

That single sentence explains a whole class of crash reports. A sign-out handler written as "always call logOut" works the first time and fails the second, because the user is already anonymous by then. It also fails for anyone who reaches a sign-out control without ever having signed in.

Guarding it with Purchases.isAnonymous, as above, is enough. What is not enough is catching the exception and moving on. If logOut fails and the app proceeds to its signed-out screens, RevenueCat still holds the paying identity, and any purchase or restore made in that window attaches to it.

Scope the listener and the cached answer to the active session

Entitlement state arrives two ways, and both outlive a sign-out unless you handle them.

Purchases.addCustomerInfoUpdateListener receives a callback whenever CustomerInfo changes on the current device, and RevenueCat says to expect it at launch and throughout the life of the app. A listener registered once at startup keeps firing across an account switch, so whatever it writes into your state must be keyed to the session it belongs to, not stored as a bare isPremium boolean.

Reads are cached. The SDK updates its cache if it is older than five minutes, and only when you call getCustomerInfo, make a purchase, or restore purchases. The SDK handles its own cache across a logIn, which is why that call returns fresh customerInfo for the new identity. The hazard is the copy you keep yourself. A provider value, a stored preference, or a widget that read the answer before the switch will happily report the previous account's access.

Treat the customerInfo returned by logIn as the first answer for the new session, discard anything held for the previous one, and remove listeners you no longer want with removeCustomerInfoUpdateListener.

What should the UI show while entitlement state is unresolved?

Three state cards after an identity change: unknown, with no answer yet, routing to a neutral loading state; no access, with an empty entitlements.active, routing to the paywall; and access, with an active entitlement, routing to the paid experience.

Show neither the paid experience nor the paywall. Between a sign-in and the first CustomerInfo for that identity there is a real third state, and giving it its own screen is cheaper than taking a feature back from someone who already saw it.

  • Unknown. Identity has changed and no entitlement answer has arrived yet. Render a neutral loading state on the gated surface. Do not route anywhere.
  • No access. An answer arrived and entitlements.active is empty for this account. Route to the paywall.
  • Access. An answer arrived and the entitlement is active. Render the paid experience.

Two states force you to pick a default, and both defaults are wrong: starting locked flashes a paywall at a subscriber, starting open hands a feature to someone who has not paid for it.

Transfer and alias settings are product decisions

A dashboard setting, not your code, decides what happens when a purchase is restored onto a different App User ID. The restore behavior setting lives in Project settings > General, and its default moves the purchase.

Restore behaviorWhat happensWho it suits
Transfer to new App User ID (default)Purchases move to the restoring App User ID. Only one customer has access at a time, so the original user loses itConsumer apps where a person moves between their own accounts
Transfer if there are no active subscriptionsTransfers only when no active subscription is involvedApps where a churned subscriber should be able to start fresh
Keep with original App User IDReturns an error when the restoring App User ID differs from the original purchaser. The documentation marks it "use with caution"Products that would rather block a restore than move access silently
Share between App User IDs (legacy)Merges App User IDs that restore the same subscription. Legacy only, and it cannot be re-enabled once you move away from itNothing new

Aliases are the same conversation from the server side. RevenueCat merges customers automatically during a logIn or a restore, and webhook payloads carry original_app_user_id alongside an aliases array holding every App User ID the subscriber has ever used. The documentation is explicit that lookups in your own systems should search both.

None of this guarantees that a person's existing purchases will follow them correctly to a new account. The setting decides, the store receipt decides, and restorePurchases is documented as linking App User IDs to any user also using those purchases. On a shared device, a restore is an identity event rather than a harmless recovery button.

Do you need to verify entitlements on your own backend?

Yes, for anything a client should not be able to grant itself. CustomerInfo decides what the app displays; your server decides what the app is allowed to do.

The chain has two links. First, identify the caller: Firebase's Admin SDK verifies an ID token with verifyIdToken(), and you take the uid from the decoded token rather than from a parameter the client supplied. Second, ask RevenueCat about that customer directly. The v2 REST API customer endpoints expose GET /projects/{project_id}/customers/{customer_id} and GET /projects/{project_id}/customers/{customer_id}/active_entitlements, both under https://api.revenuecat.com/v2 with bearer authentication.

Webhooks close the loop. A TRANSFER event means transactions and entitlements moved between App User IDs, and it carries transferred_from and transferred_to so your own records can follow the move instead of discovering it later.

For the states a subscription passes through once it exists, the subscription lifecycle states your app has to handle covers the ground after identity is settled.

A checklist for a switch that went wrong

Run this against two synthetic accounts, one paid and one not, on the same device.

  1. Read Purchases.appUserID immediately after each sign-in. It should equal that account's Firebase uid, every time.
  2. Read Purchases.isAnonymous after sign-out. If it is false, logOut did not run or did not finish.
  3. Confirm the sign-out path actually reaches logOut and does not swallow its exception.
  4. Read entitlements.active for the unpaid account. It should be empty.
  5. Check the restore behavior in Project settings > General against what you expect to happen when the same receipt reaches a second account.
  6. Look for a TRANSFER webhook you did not intend, and read transferred_from and transferred_to.
  7. Kill the app and cold start it on each account. A cached answer that survives a restart is the one worth chasing.

Where this advice stops

FlutterFlow's built-in RevenueCat actions are Paywall, Purchase and Restore Purchases. None of them manages the App User ID, so the identity work above is custom code in a FlutterFlow project rather than a toggle.

Platform support is worth checking yourself before you promise anything. RevenueCat's Flutter installation page lists web support alongside iOS and Android when a billing engine is configured, while FlutterFlow's documentation states that the underlying RevenueCat package does not support web and its integration covers iOS and Android only. Those are two primary sources describing different scopes, so verify against the versions you actually ship. The same page pins the practical floor: a purchases_flutter 10.10.0 example, iOS 13.0 or higher, Flutter 3.22 or later, and FlutterFragmentActivity on Android.

Every snippet here is a reference implementation written against the published API reference. None of it was executed against a store sandbox, a live RevenueCat project, or a real purchase while this article was written, and nothing about identity behaviour can be proven by a passing build.

Open your sign-out handler and count the calls inside it. If there is exactly one and it belongs to Firebase, you already know which account is going to see premium next.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

How do I link a Firebase UID to RevenueCat in Flutter?

Call Purchases.logIn with the Firebase uid as soon as the user is signed in, and Purchases.logOut before you sign them out. logIn takes a String App User ID and returns a LogInResult carrying fresh CustomerInfo for that identity plus a created flag, which is true the first time RevenueCat has seen that identifier. The uid is a good choice because it meets the constraints RevenueCat states for an App User ID: unique per user, opaque, stable and short. RevenueCat does not name Firebase specifically, so treat the pairing as a design decision rather than a documented instruction, and use the same identifier everywhere so one account never resolves to two customers.

Why does RevenueCat still show an active subscription after signing out?

Because signing out of Firebase Authentication does not change the RevenueCat App User ID. The two are separate identity systems, and RevenueCat keeps answering for the previous identifier until your code calls logOut or logIn. Entitlement answers also persist in two places: the SDK cache, which refreshes only when it is older than five minutes and only on getCustomerInfo, a purchase or a restore, and any copy your own app stored, such as a provider value or a saved preference. Clear the second one on every identity change and take the CustomerInfo returned by logIn as the new session's first answer.

Does Purchases.logOut() throw an error in Flutter?

Yes. The Flutter API reference documents that logOut throws a PlatformException if there was a problem restoring transactions, or if the method is called while the current user is anonymous. That second case catches a sign-out handler written as an unconditional logOut call, which works once and then fails, because the user is already anonymous after the first sign-out. Guard the call with Purchases.isAnonymous and only call logOut when it returns false. Do not silently swallow the exception either: if logOut fails and the app continues to its signed-out screens, the paying identity is still attached.

Will a user's purchases transfer when they sign in on another account?

That is decided by the restore behavior setting in your RevenueCat project, not by your app code. It lives in Project settings > General and offers four options: Transfer to new App User ID, which is the default and moves the purchase so only one customer has access at a time; Transfer if there are no active subscriptions; Keep with original App User ID, which returns an error when the restoring App User ID differs from the original purchaser; and the legacy Share between App User IDs. Read the setting before you promise anything, because the default removes access from the original user.

Can FlutterFlow switch RevenueCat users without custom code?

No. FlutterFlow's documented built-in RevenueCat actions are Paywall, Purchase and Restore Purchases, and none of them manages the App User ID. Identity work therefore means custom Dart in a FlutterFlow project, called from your existing sign-in and sign-out logic. Check platform support before you commit as well: FlutterFlow documents its RevenueCat integration as iOS and Android only and states that the underlying package does not support web, while RevenueCat's own Flutter installation page lists web support when a billing engine is configured. Verify against the versions you actually ship.