
Revenuecat Identify User
- 300 installs
- 55 repo stars
- Updated August 3, 2026
- revenuecat/ai-toolkit
Identify and alias RevenueCat app users with auth IDs so purchases, entitlements, and support data stay consistent across devices, logins, and account merges.
About
Shows agents how to identify RevenueCat users correctly: set app user IDs on login, alias anonymous purchasers, attach attributes, handle logout resets, and keep entitlements aligned with your auth system across devices.
- App User ID strategy
- Login and logout sync
- Alias across anonymous IDs
- Custom subscriber attributes
- Cross-device entitlement continuity
Revenuecat Identify User by the numbers
- 300 all-time installs (skills.sh)
- +34 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,348 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/revenuecat/ai-toolkit --skill revenuecat-identify-userAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 300 |
|---|---|
| repo stars | ★ 55 |
| Last updated | August 3, 2026 |
| Repository | revenuecat/ai-toolkit ↗ |
What it does
Identify and alias RevenueCat app users with auth IDs so purchases, entitlements, and support data stay consistent across devices, logins, and account merges.
Files
revenuecat-identify-user: connect RevenueCat to your auth system
Use this skill when the user wants to call logIn / logOut on the RevenueCat SDK so that their app users line up with RevenueCat subscribers. This skill does not cover initial SDK setup (see integrate-revenuecat), purchases (revenuecat-purchase-flow), or gating (revenuecat-entitlements-gate).
1. Detect the platform
Inspect the working directory and pick the first match, from top to bottom:
1. React Native: package.json has a react-native-purchases entry, or react-native as a dependency → read platforms/react-native.md. If expo is also a dependency, note it as an Expo project. 2. Flutter: pubspec.yaml exists at the project root → read platforms/flutter.md. 3. Kotlin Multiplatform: build.gradle.kts contains a kotlin { … } multiplatform source sets block, or depends on com.revenuecat.purchases:purchases-kmp* → read platforms/kmp.md. 4. Android (native): build.gradle(.kts) applies com.android.application (and is not KMP) → read platforms/android.md. 5. iOS (native): Package.swift, *.xcodeproj, *.xcworkspace, or Podfile at the project root → read platforms/ios.md.
If several match (e.g. an ios/ folder inside a Flutter project), pick the outermost project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
2. Shared concepts (all platforms)
- Anonymous by default. Before
logInis called, RevenueCat assigns a stable anonymous ID prefixed$RCAnonymousID:. Purchases made while anonymous are aliased onto the realappUserIDthe first timelogInis called with it, so there is no "lost purchase" risk from letting users buy before signing in. - Never use email, phone number, or a sequential database id as the appUserID. Use a stable opaque value such as your backend's user UUID, or a hash of the user id. RevenueCat treats the ID as an opaque string and it is difficult to change later.
- Call `logIn` after your auth system confirms the session. Do not call
logInspeculatively. The typical trigger is your auth state listener firing with a signed in user.logInreturns both the user's currentCustomerInfoand acreated: Booleanthat tells you whether this is a brand new RevenueCat customer. - `logOut` only works on identified users. Calling
logOutwhile the SDK is on an anonymous ID throws an error in every SDK (PurchasesErrorCode.LogOutWithAnonymousUserErroror the iOS equivalent). Gate it behind your own "is signed in" flag. - Restore is not login.
restorePurchases()asks the store for the current receipt and attaches it to the current RevenueCat user. It does not switch identities. If the user signs in on a new device, calllogIn(appUserID)first, thenrestorePurchases()only if they also expect to pull a receipt from the current store account. - Account switching is `logOut` then `logIn`. If your app lets a user sign out and sign back in as someone else, call
logOut()first, wait for it, thenlogIn(newId). Do not try to swap directly with a secondlogIn, since that will alias the two IDs together. - Configure first.
Purchases.configure(…)must have run beforelogIn/logOut. If it has not, the SDK throws.
3. Implementation
Read the platform file that matches detection:
platforms/ios.mdplatforms/android.mdplatforms/kmp.mdplatforms/flutter.mdplatforms/react-native.md
Each platform file shows the logIn and logOut calls wired into a typical auth state observer.
4. Verify
Do not claim identity sync works until:
1. In the RevenueCat dashboard, the Customer page for your test user shows the same appUserID your backend uses, not the $RCAnonymousID: placeholder. 2. Signing out clears the ID back to a fresh anonymous user; signing in as a different account switches to that account's purchases (or shows none if it is a new account). 3. A purchase made while anonymous, followed by logIn, remains attached to the signed in user (aliased, not lost). 4. Calling logOut while already anonymous is handled, not treated as a crash or a silent success.
revenuecat-identify-user: Android (native Kotlin)
Log in
Use the awaitLogIn coroutine extension. It returns a LogInResult(customerInfo, created) and throws PurchasesException on failure.
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.awaitLogIn
suspend fun syncRevenueCat(appUserID: String) {
try {
val result = Purchases.sharedInstance.awaitLogIn(appUserID)
// result.customerInfo is the current entitlement state for this user.
// result.created is true the first time this appUserID reaches RevenueCat.
} catch (e: PurchasesException) {
// Log and surface to your error pipeline; do not block the sign-in flow.
}
}Log out
awaitLogOut throws PurchasesException with PurchasesErrorCode.LogOutWithAnonymousUserError if the current user is already anonymous. Gate on Purchases.sharedInstance.isAnonymous to avoid it.
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.awaitLogOut
suspend fun signOutRevenueCat() {
if (Purchases.sharedInstance.isAnonymous) return
try {
Purchases.sharedInstance.awaitLogOut()
} catch (e: PurchasesException) {
// Log; usually safe to ignore. The user is signing out anyway.
}
}Wire it to your auth listener
Trigger the calls from wherever your app observes auth state. A typical pattern with a StateFlow<String?> of the current user id:
class AuthObserver(
private val scope: CoroutineScope,
private val currentUserID: StateFlow<String?>,
) {
fun start() {
var previous: String? = null
scope.launch {
currentUserID.collect { next ->
when {
previous == null && next != null ->
syncRevenueCat(next)
previous != null && next == null ->
signOutRevenueCat()
previous != null && next != null && previous != next -> {
signOutRevenueCat()
syncRevenueCat(next)
}
}
previous = next
}
}
}
}Callback variants (Java friendly)
Purchases.sharedInstance.logIn(
appUserID,
object : LogInCallback {
override fun onReceived(customerInfo: CustomerInfo, created: Boolean) { /* … */ }
override fun onError(error: PurchasesError) { /* … */ }
}
)
Purchases.sharedInstance.logOut(object : ReceiveCustomerInfoCallback {
override fun onReceived(customerInfo: CustomerInfo) { /* … */ }
override fun onError(error: PurchasesError) { /* … */ }
})Notes
- Use a stable opaque identifier (UUID / hash) as the appUserID. Do not pass an email address, phone number, or a raw integer database id.
awaitLogIn/awaitLogOutare extension suspend functions onPurchasesin the packagecom.revenuecat.purchases. If your installed version exposes them in a different module, the IDE will autocomplete the correct import.- Any anonymous purchases made before
awaitLogInare aliased onto the identified user automatically on that first login.
Verify
1. Sign in your test user. The RevenueCat dashboard Customer page shows the appUserID you passed, not $RCAnonymousID:…. 2. Sign out. Purchases.sharedInstance.isAnonymous becomes true. 3. Sign in as a different user. Their entitlement state appears and the previous user's does not. 4. logcat filtered by Purchases shows the logIn / logOut lifecycle without errors.
revenuecat-identify-user: Flutter
Log in
Purchases.logIn(appUserID) resolves to LogInResult(customerInfo, created).
import 'package:flutter/services.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
Future<void> syncRevenueCat(String appUserID) async {
try {
final result = await Purchases.logIn(appUserID);
// result.customerInfo is the current entitlement state for this user.
// result.created is true the first time this appUserID reaches RevenueCat.
} on PlatformException catch (e) {
// Log and surface to your error pipeline; do not block the sign-in flow.
}
}Log out
Calling logOut while the SDK is anonymous rejects with PurchasesErrorCode.logOutWithAnonymousUserError. Guard with Purchases.isAnonymous (available on the current customer info).
Future<void> signOutRevenueCat() async {
final info = await Purchases.getCustomerInfo();
// Anonymous IDs always start with $RCAnonymousID:
if (info.originalAppUserId.startsWith(r'$RCAnonymousID:')) return;
try {
await Purchases.logOut();
} on PlatformException {
// Log; usually safe to ignore during sign-out.
}
}Wire it to your auth listener
Drive the calls from your auth stream. Example with a Stream<String?> of the current user id:
class RevenueCatIdentitySync {
String? _previous;
void attach(Stream<String?> currentUserIDStream) {
currentUserIDStream.listen((next) async {
final prev = _previous;
_previous = next;
if (prev == null && next != null) {
await syncRevenueCat(next);
} else if (prev != null && next == null) {
await signOutRevenueCat();
} else if (prev != null && next != null && prev != next) {
await signOutRevenueCat();
await syncRevenueCat(next);
}
});
}
}Notes
- Use a stable opaque identifier (UUID / hash). Do not pass an email address, phone number, or a raw integer database id.
PurchasesErrorHelper.getErrorCode(e)turns aPlatformExceptioninto aPurchasesErrorCodeenum if you want to handle specific cases. ForlogIn/logOut, treating any error as "log and continue" is usually enough.- Any purchase made anonymously before
Purchases.logInis aliased onto the identified user automatically on the first login with that id.
Verify
1. Sign in your test user. The RevenueCat dashboard Customer page shows the appUserID you passed, not $RCAnonymousID:…. 2. Sign out. A subsequent Purchases.getCustomerInfo() returns an originalAppUserId starting with $RCAnonymousID:. 3. Sign in as a different user. Their entitlement state appears and the previous user's does not. 4. flutter logs shows the Purchases native logs for logIn / logOut without errors.
revenuecat-identify-user: iOS (native)
Log in
Purchases.shared.logIn(_:) returns a tuple of (customerInfo: CustomerInfo, created: Bool). Call it from your auth state observer once you have a confirmed user id.
import RevenueCat
func syncRevenueCat(with appUserID: String) async {
do {
let result = try await Purchases.shared.logIn(appUserID)
// result.customerInfo is the current entitlement state for this user.
// result.created is true the first time this appUserID reaches RevenueCat.
if result.created {
// Optional: set initial attributes on the new RC customer.
}
} catch {
print("RevenueCat logIn failed: \(error)")
}
}Log out
logOut() throws if the current user is already anonymous. Guard it on your own signed in flag (or check Purchases.shared.isAnonymous).
func signOutRevenueCat() async {
guard !Purchases.shared.isAnonymous else { return }
do {
_ = try await Purchases.shared.logOut()
} catch {
print("RevenueCat logOut failed: \(error)")
}
}Wire it to your auth listener
The cleanest place is wherever your app receives auth state changes. Example with an @Observable auth model:
@Observable
final class AuthStore {
private(set) var currentUserID: String?
func onAuthStateChanged(newUserID: String?) async {
let previous = currentUserID
currentUserID = newUserID
switch (previous, newUserID) {
case (nil, let newID?): // signed in
await syncRevenueCat(with: newID)
case (let oldID?, let newID?) where oldID != newID: // switched account
await signOutRevenueCat()
await syncRevenueCat(with: newID)
case (_?, nil): // signed out
await signOutRevenueCat()
default:
break
}
}
}Notes
logInuses async/await on iOS 13+. The completion variant islogIn(_:completion:)with signature(CustomerInfo?, Bool, PublicError?) -> Void.- RevenueCat will alias purchases that were made while anonymous onto the logged in
appUserIDautomatically on the firstlogIncall with that id. - Use a stable opaque identifier (UUID / hash) as the appUserID. Do not pass an email address or a raw integer database id.
- If the SDK was configured with an
appUserIDup front (viaconfigure(withAPIKey:appUserID:)), you generally do not needlogInuntil that user signs out and back in as somebody else.
Verify
1. Sign in your test user. In the RevenueCat dashboard, the Customer page shows the appUserID you passed to logIn, not $RCAnonymousID:…. 2. Sign out. Purchases.shared.isAnonymous becomes true again. 3. Sign in as a different user. Their entitlement state appears and the previous user's does not. 4. Make a sandbox purchase while anonymous, then logIn. The purchase remains attached to the signed in appUserID.
revenuecat-identify-user: Kotlin Multiplatform
purchases-kmp exposes logIn / logOut from commonMain. The coroutine extensions live in com.revenuecat.purchases.kmp.ktx and return SuccessfulLogin(customerInfo, created) for login.
Log in (commonMain)
import com.revenuecat.purchases.kmp.Purchases
import com.revenuecat.purchases.kmp.ktx.awaitLogIn
import com.revenuecat.purchases.kmp.models.PurchasesException
suspend fun syncRevenueCat(appUserID: String) {
try {
val result = Purchases.sharedInstance.awaitLogIn(appUserID)
// result.customerInfo is the current entitlement state for this user.
// result.created is true the first time this appUserID reaches RevenueCat.
} catch (e: PurchasesException) {
// Log and surface to your error pipeline.
}
}Log out (commonMain)
import com.revenuecat.purchases.kmp.Purchases
import com.revenuecat.purchases.kmp.ktx.awaitLogOut
import com.revenuecat.purchases.kmp.models.PurchasesException
suspend fun signOutRevenueCat() {
if (Purchases.sharedInstance.isAnonymous) return
try {
Purchases.sharedInstance.awaitLogOut()
} catch (e: PurchasesException) {
// Log; usually safe to ignore during sign-out.
}
}Wire it to your auth listener
Call syncRevenueCat / signOutRevenueCat from commonMain whenever your auth state changes. Example using a StateFlow<String?>:
class AuthObserver(
private val scope: CoroutineScope,
private val currentUserID: StateFlow<String?>,
) {
fun start() {
var previous: String? = null
scope.launch {
currentUserID.collect { next ->
when {
previous == null && next != null ->
syncRevenueCat(next)
previous != null && next == null ->
signOutRevenueCat()
previous != null && next != null && previous != next -> {
signOutRevenueCat()
syncRevenueCat(next)
}
}
previous = next
}
}
}
}Callback variants
If you prefer to skip the coroutine dependency:
Purchases.sharedInstance.logIn(
newAppUserID = appUserID,
onError = { /* … */ },
onSuccess = { customerInfo, created -> /* … */ }
)
Purchases.sharedInstance.logOut(
onError = { /* … */ },
onSuccess = { customerInfo -> /* … */ }
)Notes
- Use a stable opaque identifier (UUID / hash). Do not pass an email address, phone number, or a raw integer database id.
Purchases.sharedInstance.isAnonymousis available in commonMain and is the correct guard before callingawaitLogOut.- If your installed version of
purchases-kmpdoes not exposeawaitLogIn/awaitLogOutincom.revenuecat.purchases.kmp.ktx, prefer what the IDE autocompletes and see thepurchases-kmpREADME. Some versions shipResult-based variants in a separate module.
Verify
1. On each target, sign in your test user. The RevenueCat dashboard Customer page shows the appUserID you passed, not $RCAnonymousID:…. 2. Sign out. isAnonymous becomes true on both iOS and Android. 3. Sign in as a different user. Their entitlement state appears on both targets. 4. Native logs (Xcode console on iOS, logcat on Android) show the logIn / logOut lifecycle. The KMP SDK wraps the native SDKs, so the logs come from them.
revenuecat-identify-user: React Native
Log in
Purchases.logIn(appUserID) resolves to { customerInfo, created }.
import Purchases from 'react-native-purchases';
export async function syncRevenueCat(appUserID: string): Promise<void> {
try {
const { customerInfo, created } = await Purchases.logIn(appUserID);
// customerInfo is the current entitlement state for this user.
// created is true the first time this appUserID reaches RevenueCat.
} catch (e) {
console.warn('RevenueCat logIn failed', e);
}
}Log out
Calling logOut while the SDK is anonymous rejects with a LogOutWithAnonymousUserError. Guard with the anonymous-id prefix.
export async function signOutRevenueCat(): Promise<void> {
const info = await Purchases.getCustomerInfo();
if (info.originalAppUserId.startsWith('$RCAnonymousID:')) return;
try {
await Purchases.logOut();
} catch (e) {
console.warn('RevenueCat logOut failed', e);
}
}Wire it to your auth state
A useEffect watching the current user id is the natural place:
import { useEffect, useRef } from 'react';
import { syncRevenueCat, signOutRevenueCat } from './revenuecatIdentity';
export function useRevenueCatIdentity(currentUserID: string | null) {
const previous = useRef<string | null>(null);
useEffect(() => {
const prev = previous.current;
previous.current = currentUserID;
(async () => {
if (prev == null && currentUserID != null) {
await syncRevenueCat(currentUserID);
} else if (prev != null && currentUserID == null) {
await signOutRevenueCat();
} else if (prev != null && currentUserID != null && prev !== currentUserID) {
await signOutRevenueCat();
await syncRevenueCat(currentUserID);
}
})();
}, [currentUserID]);
}Use it from your root component:
function Root() {
const currentUserID = useCurrentUserID(); // from your auth library
useRevenueCatIdentity(currentUserID);
return <AppNavigator />;
}Notes
- Use a stable opaque identifier (UUID / hash). Do not pass an email address, phone number, or a raw integer database id.
- Under Expo,
Purchases.logInrequires a development build. It throws in Expo Go because the native module is not linked. - Any purchase made anonymously before
Purchases.logInis aliased onto the identified user automatically on the first login with that id.
Verify
1. Sign in your test user. The RevenueCat dashboard Customer page shows the appUserID you passed, not $RCAnonymousID:…. 2. Sign out. A subsequent Purchases.getCustomerInfo() returns originalAppUserId starting with $RCAnonymousID:. 3. Sign in as a different user. Their entitlement state appears and the previous user's does not. 4. Platform logs (Xcode console on iOS, adb logcat filtered by Purchases on Android) show the logIn / logOut lifecycle.