Skip to main content

User Management

User management in Superwall involves identifying users, tracking their attributes, and managing their subscription status across app sessions.

User Identity System

Superwall uses a dual-identity system:
┌──────────────────────────────────────────┐
│           User Identity                  │
├──────────────────────────────────────────┤
│  aliasId      │ Auto-generated by SDK    │
│               │ Persists across sessions │
│               │ Device-specific          │
├───────────────┼──────────────────────────┤
│  appUserId    │ Your user ID             │
│               │ Set via identify()       │
│               │ Links to your backend    │
└──────────────────────────────────────────┘

aliasId

  • Automatically generated when SDK initializes
  • Device-specific identifier
  • Persists across app restarts
  • Used for anonymous users, experiment assignments

appUserId

  • Your user ID from your authentication system
  • Set explicitly via identify()
  • Links device to user account
  • Used for cross-device tracking, user-specific campaigns

The useUser Hook

The useUser hook provides user management functionality:
import { useUser } from "expo-superwall";

function UserProfile() {
  const {
    identify,
    update,
    signOut,
    refresh,
    user,
    subscriptionStatus,
  } = useUser();

  return (
    <View>
      <Text>User ID: {user?.appUserId}</Text>
      <Text>Status: {subscriptionStatus?.status}</Text>
    </View>
  );
}

Return Values

interface UseUserReturn {
  // Actions
  identify: (userId: string, options?: IdentifyOptions) => Promise<void>;
  update: (attrs: Record<string, any> | ((old) => Record<string, any>)) => Promise<void>;
  signOut: () => void;
  refresh: () => Promise<Record<string, any>>;
  setSubscriptionStatus: (status: SubscriptionStatus) => Promise<void>;
  setIntegrationAttributes: (attrs: IntegrationAttributes) => Promise<void>;
  getIntegrationAttributes: () => Promise<Record<string, string>>;
  getEntitlements: () => Promise<EntitlementsInfo>;

  // State
  user?: UserAttributes | null;
  subscriptionStatus?: SubscriptionStatus;
}

Identifying Users

Identify a user when they log in:
const { identify } = useUser();

const handleLogin = async (email: string, password: string) => {
  // Your auth logic
  const user = await authService.login(email, password);

  // Identify with Superwall
  await identify(user.id);

  // Optionally set attributes immediately
  await update({
    email: user.email,
    name: user.name,
    tier: user.subscriptionTier,
  });
};

IdentifyOptions

await identify(userId, {
  restorePaywallAssignments: true, // Restore A/B test assignments
});
restorePaywallAssignments:
  • true - Restores user’s previous paywall variant assignments
  • false (default) - Assigns new variants
  • Use when: Users frequently switch accounts or reinstall app
Restoring assignments ensures consistent A/B test experiences across devices and sessions.

User Attributes

UserAttributes Interface

interface UserAttributes {
  // System attributes (read-only)
  aliasId: string;                    // SDK-generated ID
  appUserId: string;                  // Your user ID
  applicationInstalledAt: string;     // ISO date string
  seed: number;                       // For experiment bucketing

  // Custom attributes (read-write)
  [key: string]: any | null;
}

Setting Attributes

const { update } = useUser();

// Set attributes directly
await update({
  name: "John Doe",
  email: "[email protected]",
  age: 28,
  isPremium: false,
  lastActiveAt: new Date().toISOString(),
});

// Update based on previous values
await update((old) => ({
  ...old,
  loginCount: (old.loginCount || 0) + 1,
  lastLogin: Date.now(),
}));

Supported Value Types

TypeExampleNotes
String"John Doe"Any string value
Number28, 3.14Integer or float
Booleantrue, falseFor flags
Datenew Date()Converted to ISO string
nullnullRemoves attribute
Arrays and nested objects are not supported and will be dropped.

Reserved Attributes

Attributes starting with $ are reserved for Superwall:
// These will be ignored
await update({
  $customField: "value",  // ❌ Dropped
  customField: "value",   // ✅ Accepted
});

Attribute Use Cases

Campaign Targeting

Use attributes in dashboard audience filters:
await update({
  subscriptionTier: "pro",
  totalPurchases: 5,
  country: "US",
  registeredDays: 30,
});
Dashboard Rule:
Show paywall if:
  subscriptionTier != "pro" AND
  totalPurchases < 3 AND
  country == "US"

Personalization

Reference attributes in paywall content:
await update({
  firstName: "Sarah",
  favoriteFeature: "fishing",
});
Paywall Template:
<h1>Hey {{ user.firstName }}, upgrade to unlock {{ user.favoriteFeature }}!</h1>

Analytics Integration

const { update } = useUser();

useEffect(() => {
  update({
    amplitudeDeviceId: Analytics.getDeviceId(),
    mixpanelDistinctId: Mixpanel.getDistinctId(),
  });
}, []);

Refreshing User Data

Manually fetch fresh user attributes:
const { refresh, user } = useUser();

const syncUserData = async () => {
  const freshAttrs = await refresh();
  console.log("Updated attributes:", freshAttrs);
};
Refreshing:
  • Fetches latest attributes from Superwall servers
  • Updates subscription status
  • Returns fresh attribute object

Signing Out

Reset user identity when logging out:
const { signOut } = useUser();

const handleLogout = async () => {
  // Your logout logic
  await authService.logout();

  // Reset Superwall identity
  signOut();

  // SDK generates new aliasId
  // appUserId is cleared
  // Attributes are reset
};
What happens:
  1. Current user identity is cleared
  2. New aliasId is generated
  3. appUserId is reset
  4. Custom attributes are cleared
  5. Paywall assignments may change
Always call signOut() when users log out to ensure accurate analytics and targeting.

Integration Attributes

Link third-party attribution and analytics platform IDs:
const { setIntegrationAttributes, getIntegrationAttributes } = useUser();

// Set integration IDs
await setIntegrationAttributes({
  adjustId: "adjust_123",
  amplitudeUserId: "amp_456",
  firebaseAppInstanceId: "firebase_789",
  mixpanelDistinctId: "mp_abc",
});

// Retrieve them later
const integrations = await getIntegrationAttributes();
console.log(integrations.adjustId); // "adjust_123"

Supported Integrations

  • Attribution: Adjust, AppsFlyer, Kochava, Tenjin
  • Analytics: Amplitude, Mixpanel, Posthog, mParticle
  • Marketing: Braze, Iterable, Customer.io, OneSignal, Airship
  • Other: Firebase, Facebook, CleverTap
Full list:
type IntegrationAttribute =
  | "adjustId"
  | "amplitudeDeviceId"
  | "amplitudeUserId"
  | "appsflyerId"
  | "brazeAliasName"
  | "brazeAliasLabel"
  | "onesignalId"
  | "fbAnonId"
  | "firebaseAppInstanceId"
  | "iterableUserId"
  | "mixpanelDistinctId"
  | "mparticleId"
  | "clevertapId"
  | "airshipChannelId"
  | "kochavaDeviceId"
  | "tenjinId"
  | "posthogUserId"
  | "customerioId";

User Attributes vs Placement Params

AspectUser AttributesPlacement Params
ScopeUser-level, persistentPlacement-specific, ephemeral
LifetimeAcross sessionsSingle placement call
Use CaseUser demographics, behaviorContextual data
Set Viaupdate()registerPlacement({ params })
Example:
// User attributes - persistent
await update({
  subscriptionTier: "free",
  country: "US",
});

// Placement params - contextual
await registerPlacement({
  placement: "article_read",
  params: {
    articleId: "123",
    category: "tech",
    wordCount: 1500,
  },
});
Dashboard can target on both:
Show paywall if:
  user.subscriptionTier == "free" AND
  params.category == "tech" AND
  params.wordCount > 1000

Accessing User State

Reactive Access

const { user, subscriptionStatus } = useUser();

// Component re-renders when user or status changes
return (
  <View>
    <Text>Welcome, {user?.appUserId}</Text>
    <Text>Status: {subscriptionStatus?.status}</Text>
  </View>
);

Direct Access

const { getUserAttributes } = useSuperwall();

// Fetch without subscribing to updates
const attrs = await getUserAttributes();
console.log(attrs);

Common Patterns

Auto-Identify on App Start

function App() {
  const { identify } = useUser();

  useEffect(() => {
    const initUser = async () => {
      const userId = await AsyncStorage.getItem("userId");
      if (userId) {
        await identify(userId);
      }
    };
    initUser();
  }, []);

  return <AppContent />;
}

Sync Attributes Periodically

function UserSync() {
  const { update } = useUser();

  useEffect(() => {
    const syncAttributes = async () => {
      const userData = await fetchUserProfile();
      await update({
        name: userData.name,
        email: userData.email,
        subscriptionTier: userData.tier,
      });
    };

    // Sync on mount and every 5 minutes
    syncAttributes();
    const interval = setInterval(syncAttributes, 5 * 60 * 1000);

    return () => clearInterval(interval);
  }, []);

  return null;
}

Track User Behavior

function FeatureUsageTracker() {
  const { update } = useUser();

  const trackFeatureUse = async (feature: string) => {
    await update((old) => ({
      ...old,
      [`${feature}UseCount`]: (old[`${feature}UseCount`] || 0) + 1,
      lastUsedFeature: feature,
      lastActiveAt: new Date().toISOString(),
    }));
  };

  return { trackFeatureUse };
}

Compat SDK Equivalent

For those using the Compat SDK:
import Superwall from "expo-superwall/compat";

// Identify
await Superwall.shared.identify({
  userId: "user_123",
  options: { restorePaywallAssignments: true },
});

// Set attributes
await Superwall.shared.setUserAttributes({
  name: "John Doe",
  tier: "premium",
});

// Get attributes
const attrs = await Superwall.shared.getUserAttributes();

// Reset
await Superwall.shared.reset();

// Integration attributes
await Superwall.shared.setIntegrationAttributes({
  adjustId: "adjust_123",
});

Best Practices

1. Identify Early

// ✅ Good - Identify as soon as you have userId
useEffect(() => {
  if (isAuthenticated && userId) {
    identify(userId);
  }
}, [isAuthenticated, userId]);

// ❌ Bad - Waiting too long
// Don't wait until first paywall

2. Keep Attributes Updated

// ✅ Good - Update when data changes
useEffect(() => {
  if (userProfile) {
    update({
      tier: userProfile.subscriptionTier,
      lastSyncAt: Date.now(),
    });
  }
}, [userProfile]);

3. Don’t Store Sensitive Data

// ❌ Bad - Sensitive data
await update({
  creditCardNumber: "1234...",
  ssn: "123-45-6789",
});

// ✅ Good - Non-sensitive metadata
await update({
  hasPaymentMethod: true,
  accountCreatedAt: date.toISOString(),
});

4. Use Meaningful Names

// ✅ Good - Descriptive
await update({
  subscriptionTier: "pro",
  totalArticlesRead: 42,
  preferredLanguage: "en",
});

// ❌ Bad - Cryptic
await update({
  t: "pro",
  c: 42,
  l: "en",
});

Further Reading

Build docs developers (and LLMs) love