npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@despia/user-identity

v1.0.5

Published

Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.

Readme

@despia/user-identity

Repository


What this package is

@despia/user-identity is the standard way to get and persist a single, stable user ID (app_user_id) in Despia native apps (iOS and Android). It does only the following:

  1. Resolves who the user is: vault first (device’s stored identity — iCloud/Android backup). If nothing in the vault, then restore from purchase history (RevenueCat), then fall back to a new install ID.
  2. Persists that ID in the device vault so it survives app restarts and reinstalls (when backup is used).
  3. Returns that ID to you so you can pass it to RevenueCat, OneSignal, your backend, and any other service.

This package does not register users with your backend, sync to OneSignal, or call RevenueCat. It only gives you the app_user_id; you send it to your backend and other services yourself. It does not require users to sign in; it works without accounts or passwords.


What are user sessions in Despia?

Every Despia app needs one consistent user identifier (app_user_id) that:

  • Stays the same across app restarts, reinstalls, and device changes
  • Links all your services together (purchases, push notifications, backend, etc.)
  • Works without requiring users to sign in - no password, no account, no login screen

This package gives you that identifier. It’s the standard way Despia apps manage user identity.


Why this matters

Without a stable identity, users get “lost” when they reinstall. They lose subscriptions, trials, credits, or preferences. You can’t recognize them or enforce your rules.

This package solves that by using:

  1. iCloud Key-Value store (iOS) and Android backup - identity survives reinstall when the user backs up
  2. Restore purchases - if they bought something before, identity is recovered from purchase history
  3. Install ID - fallback for brand-new users

You get a stable app_user_id without forcing users to create an account. They keep access to what they paid for.

It also helps prevent fraud. The same identity lets you detect when someone uninstalls and creates a new account to claim another free trial or bonus credits. You can enforce limits and keep paying users correctly linked.


Install

Install the package and the Despia native SDK (required):

# npm
npm install @despia/user-identity despia-native

# pnpm
pnpm add @despia/user-identity despia-native

# yarn
yarn add @despia/user-identity despia-native

You must have despia-native in your app; this package uses it and does not bundle it (no duplicate React or Despia instance).


Detailed step-by-step instructions

Follow these steps in order. Do not show paywalls, push registration, or any user-specific UI until identity is ready.

Step 1: Resolve identity at app entry

Where: The first place your app runs (e.g. root layout, App.tsx, or main entry before any route that needs the user).

What to do:

  1. Import the package and despia-native:

    import despia from 'despia-native';
    import { userIdentity, isDespia, getPlatform } from '@despia/user-identity';
  2. Call userIdentity() once and await it. This reads the vault and/or purchase history and returns the user ID (or creates a new one).

    const result = await userIdentity();
  3. Check the result:

    • If result === null: the app is running on web (not in Despia). Use your own web identity (session, cookies, etc.) and skip the steps below for native.
    • If result is an object: you are in the Despia native app. Continue.
  4. From result you get: appUserId, installId, source, aliases. Use appUserId everywhere you need a user identifier (RevenueCat, OneSignal, backend). The source tells you where the ID came from: 'vault' (restored from backup), 'restore' (from purchase history), or 'new' (first install).

Step 2: Send the ID to your backend (do not block the UI)

Important: Do not await the backend call. If you await it, a slow or cold-starting backend can block your app for minutes. Fire the request and forget.

  1. Call your register endpoint with the identity payload. Example:

    if (result) {
      const { appUserId, installId, source } = result;
      const platform = getPlatform(); // 'ios' | 'android' | null
      fetch('https://your-api.com/api/user/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          appUserId,
          deviceId: installId,
          source,
          platform: platform ?? 'unknown',
          timestamp: new Date().toISOString(),
        }),
      }).catch(() => {}); // fire-and-forget; do not await
    }
  2. Your backend should upsert a user by app_user_id and link the device. The user can use the app immediately; the backend just needs to receive the data eventually.

Step 3: Sync the ID to RevenueCat and OneSignal

Use the same appUserId so purchases and push are tied to one user.

  1. RevenueCat: Whenever you open a paywall or set the user in RevenueCat, pass appUserId as the external_id. Example:

    despia(`revenuecat://launchPaywall?external_id=${appUserId}&offering=default`);

    If you do not pass this external_id, restore-from-purchases and identity recovery will not work correctly.

  2. OneSignal: Set the external user ID so push is linked to this user. Example (also fire-and-forget; do not block):

    despia(`setonesignalplayerid://?user_id=${appUserId}`);

Step 4: Add a "Restore purchases" button

App Store and Play Store require a way to restore purchases. Add a button (e.g. on Settings or the paywall) that:

  1. Calls restore() from this package:

    import { restore } from '@despia/user-identity';
    const restored = await restore();
  2. If restored is not null, you get restored.appUserId. Sync it to OneSignal and your backend (again, fire-and-forget). If restored is null, no purchase with an externalUserId was found; show a message like "No purchases found."

Step 5: If you have login, wire it to identity

When the user signs in, your backend returns the canonical app_user_id (CLAIM or RECOVER). Persist it with this package so future launches use it:

  1. After a successful login response that includes appUserId:

    import { setAppUserId } from '@despia/user-identity';
    await setAppUserId(appUserIdFromBackend);
  2. Then sync that ID to OneSignal and your backend as in Steps 2 and 3.


Usage (full example)

import despia from 'despia-native';
import { userIdentity, restore, isDespia, getPlatform } from '@despia/user-identity';

// At app entry - before any paywall, push, or user-specific feature
const result = await userIdentity();

if (result) {
  const { appUserId, installId, source, aliases } = result;

  // Sync to services (fire-and-forget; do not await)
  despia(`revenuecat://launchPaywall?external_id=${appUserId}&offering=default`);
  despia(`setonesignalplayerid://?user_id=${appUserId}`);
  fetch('/api/user/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      appUserId,
      deviceId: installId,
      source,
      platform: getPlatform() ?? 'unknown',
      timestamp: new Date().toISOString(),
    }),
  }).catch(() => {});
} else {
  // Web: use your own identity (session, etc.)
}

// Restore purchases button (e.g. in Settings)
async function onRestorePurchases() {
  const restored = await restore();
  if (restored) {
    const { appUserId } = restored;
    despia(`setonesignalplayerid://?user_id=${appUserId}`);
    fetch('/api/user/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ appUserId, source: 'restore_button', timestamp: new Date().toISOString() }),
    }).catch(() => {});
  }
}

Do not block app load on backend or OneSignal. Call userIdentity() and then fire registration and OneSignal in the background without awaiting them.


When each source is used

| Source | When it happens | |----------|------------------| | vault | User had this device before and iCloud/Android backup restored the stored ID | | restore| Vault was empty but purchase history had an externalUserId (from RevenueCat) - we recovered it | | new | First install, no backup, no purchases - we use the device install ID |


User ID flows (enterprise reference)

This section documents all identity flows end-to-end for integration, audits, and support.

Flow quick reference

| Flow | Trigger | Source | Outcome | |------|---------|--------|---------| | 1 | Every app launch | - | Resolution: vault → restore → new | | 2 | First install | new | Install ID used, persisted to vault | | 3 | Reinstall + backup restored | vault | Same ID from iCloud/Android | | 4 | Reinstall, no backup | restore | Recovered from purchase history | | 5 | New device | restore or new | Purchase sync or install ID | | 6 | Login, account new | CLAIM | Device ID linked to account | | 7 | Login, account exists | RECOVER | Device switches to account ID | | 8 | Restore purchases button | - | Recovered from store, vault updated | | 9 | Backend merge | - | linkAlias() for fraud/audit |


Flow 1: Identity resolution (every app launch)

Vault first (highest priority). We only restore from purchases or use install ID when the vault is empty. userIdentity() runs this logic in order:

flowchart TD
    A[Read Storage Vault<br/>iCloud / Android backup] --> B{Found?}
    B -->|Yes| C[Use it. source = vault]
    B -->|No| D[Query purchase history<br/>RevenueCat / App Store / Play Store]
    D --> E{Any purchase with<br/>externalUserId?}
    E -->|Yes| F[Use it. source = restore<br/>Lock identity to vault]
    E -->|No| G[Fallback to install ID<br/>source = new]
    C --> H[Persist to vault]
    F --> H
    G --> H
    H --> I[Return appUserId, installId, source, aliases]

Critical: Always pass appUserId as external_id when launching RevenueCat paywalls. Otherwise step 2 will never find a recoverable ID.


Flow 2: First-time user (no backup, no purchases)

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | App launches, vault empty | - | - | | 2 | userIdentity() → vault empty, no purchases | - | - | | 3 | Uses install ID, source: 'new' | - | - | | 4 | Persists install ID to vault | - | app_user_id = install ID | | 5 | POST /api/user/register | Upsert user, link device | - | | 6 | Sync to RevenueCat, OneSignal | - | - |

Result: appUserId = install ID. Same user on same device will keep this ID across restarts.


Flow 3: Returning user - reinstall with iCloud/Android backup

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | User reinstalls app | - | - | | 2 | iCloud/Android restores vault before app runs | - | app_user_id = previous ID | | 3 | userIdentity() reads vault | - | - | | 4 | Returns source: 'vault' | - | - | | 5 | POST /api/user/register | Recognizes existing user | - |

Result: Same appUserId as before reinstall. Subscriptions, credits, and preferences preserved.


Flow 4: Returning user - reinstall without backup (restore from purchases)

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | User reinstalls, backup not yet restored | - | Empty | | 2 | userIdentity() → vault empty | - | - | | 3 | Queries purchase history | - | - | | 4 | Finds purchase with externalUserId (from previous paywall launch) | - | - | | 5 | Uses that ID, source: 'restore', locks to vault | - | app_user_id = recovered ID | | 6 | POST /api/user/register | Recognizes existing user | - |

Result: Same appUserId as when they originally purchased. Requires purchases were made with external_id = app_user_id.


Flow 5: New device, same Apple/Google account

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | User gets new phone, installs app | - | Empty (new device) | | 2 | Purchase history may sync from App Store / Play Store | - | - | | 3 | If purchase history has externalUserIdsource: 'restore' | - | Recovered ID written | | 4 | If not yet synced → source: 'new', install ID used | - | Install ID written | | 5 | User taps "Restore purchases" later | - | - | | 6 | restore() finds purchase with externalUserId | - | Vault updated | | 7 | Sync to backend, OneSignal, RevenueCat | - | - |

Result: Identity recovered when store syncs purchase history, or when user restores purchases.


Flow 6: Login - CLAIM (account has no app_user_id)

User signs in for the first time. Their account has no linked app_user_id.

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | userIdentity() returns appUserId (e.g. install ID) | - | - | | 2 | User logs in | - | - | | 3 | POST /api/user/login { accountId, currentAppUserId } | - | - | | 4 | Backend: account has no app_user_id | - | - | | 5 | Backend: CLAIM - save currentAppUserId to account | - | - | | 6 | Returns { appUserId: currentAppUserId, action: 'claimed' } | - | - | | 7 | Client calls setAppUserId(appUserId) | - | Same ID (already in vault) | | 8 | Sync to RevenueCat, OneSignal | - | - |

Result: Anonymous device identity is now linked to the account. Future logins from this device use this ID.


Flow 7: Login - RECOVER (account already has app_user_id)

User signs in on a new device. Their account already has an app_user_id from another device.

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | New device: userIdentity() returns appUserId = install ID | - | - | | 2 | User logs in | - | - | | 3 | POST /api/user/login { accountId, currentAppUserId } | - | - | | 4 | Backend: account has app_user_id = "user-abc" | - | - | | 5 | Backend: RECOVER - return existing ID | - | - | | 6 | Returns { appUserId: "user-abc", action: 'recovered' } | - | - | | 7 | Client calls setAppUserId("user-abc") | - | Vault overwritten with "user-abc" | | 8 | Sync to RevenueCat, OneSignal | - | - |

Result: Device now uses the account’s canonical ID. Purchases and entitlements from other devices apply here.


Flow 8: Restore purchases button

Required by App Store and Play Store. User taps "Restore purchases" (e.g. in Settings).

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | User taps "Restore purchases" | - | - | | 2 | restore() queries purchase history | - | - | | 3 | Finds purchase with externalUserId | - | - | | 4 | Writes ID to vault, locks identity | - | app_user_id updated | | 5 | Returns { appUserId, aliases } | - | - | | 6 | Client syncs appUserId to RevenueCat, OneSignal, POST /api/user/register | - | - |

Result: Identity recovered from store. Use source: 'restore_button' when registering so backend can attribute the source.


Flow 9: Aliases and fraud detection

When backend detects the same person under multiple IDs (e.g. same device, same receipt):

| Step | Client | Backend | Vault | |------|--------|---------|-------| | 1 | Backend merges IDs, returns instruction to link | - | - | | 2 | Client calls linkAlias(oldInstallId) | - | Aliases array updated | | 3 | Next userIdentity() returns { appUserId, aliases: [oldInstallId] } | - | - | | 4 | Send aliases to backend for fraud checks | - | - |

Result: Main ID stays canonical. Aliases support auditing and fraud prevention.


Backend decision logic: CLAIM vs RECOVER

flowchart TD
    A[POST /api/user/login] --> B[Look up account by accountId]
    B --> C{Does account have<br/>app_user_id?}
    C -->|No| D[CLAIM]
    D --> E[Save currentAppUserId to account]
    E --> F[Return appUserId: currentAppUserId<br/>action: claimed]
    C -->|Yes| G[RECOVER]
    G --> H[Return appUserId: account.app_user_id<br/>action: recovered]

Service sync order (recommended)

After userIdentity() or restore() or setAppUserId():

  1. RevenueCat - despia('revenuecat://launchPaywall?external_id=' + appUserId) (or set external ID via SDK)
  2. OneSignal - despia('setonesignalplayerid://?user_id=' + appUserId)
  3. Backend - POST /api/user/register with appUserId, deviceId, source, platform, timestamp

Backend integration

Your backend stores app_user_id as the main user identifier. It receives it in two flows:

1. Register (on every app launch)

On each launch, the app sends the resolved app_user_id to your backend so you can track devices and know who’s active:

// Client (after userIdentity())
await fetch('/api/user/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    appUserId,
    deviceId: installId,
    source,        // 'vault' | 'restore' | 'new'
    platform: 'ios' | 'android',
    timestamp: new Date().toISOString()
  })
});

Your backend upserts a user and links the device. Use this for analytics, last-seen, and knowing which devices belong to whom.

2. Login (CLAIM vs RECOVER)

When a user signs in, the backend must decide whether to CLAIM or RECOVER:

| Action | When | What the backend does | |--------|------|------------------------| | CLAIM | Account has no app_user_id yet | Link the current app_user_id from the device to this account | | RECOVER | Account already has an app_user_id | Return that existing ID so the device switches to it |

The client sends currentAppUserId (from userIdentity()). The backend replies with the canonical app_user_id to use. The client then calls setAppUserId(returnedId) so the vault and future resolution use it:

// Client
const { appUserId, action } = await fetch('/api/user/login', {
  method: 'POST',
  body: JSON.stringify({
    accountId: 'user-123',
    currentAppUserId: result.appUserId,
    credentials: { email, password }
  })
}).then(r => r.json());

await setAppUserId(appUserId);
// action is 'claimed' or 'recovered'

The backend should store app_user_id as the primary key or foreign key for that user. RevenueCat, OneSignal, webhooks, and any other service should map back to this ID.

Minimal database schema

-- Users: app_user_id is the main identifier
CREATE TABLE users (
  app_user_id TEXT PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  last_seen TIMESTAMPTZ DEFAULT NOW()
);

-- Devices: link device_id to app_user_id
CREATE TABLE user_devices (
  device_id TEXT PRIMARY KEY,
  app_user_id TEXT REFERENCES users(app_user_id),
  platform TEXT,  -- 'ios' | 'android'
  last_seen TIMESTAMPTZ DEFAULT NOW()
);

-- Accounts (if you have login): link account to app_user_id
CREATE TABLE accounts (
  id UUID PRIMARY KEY,
  app_user_id TEXT REFERENCES users(app_user_id),
  email TEXT,
  ...
);

Register endpoint: upsert into users and user_devices on each POST. Login endpoint: look up account, return app_user_id (CLAIM or RECOVER).

3. RevenueCat webhook (grant access on purchase)

This package does not call RevenueCat; it only gives you app_user_id. To grant access when a user pays, use a RevenueCat webhook on your backend.

  1. In RevenueCat, set the webhook URL to your backend (e.g. https://your-api.com/webhooks/revenuecat).
  2. RevenueCat sends events (e.g. INITIAL_PURCHASE, RENEWAL) with app_user_id (RevenueCat calls it app_user_id or the external ID you set).
  3. Your backend finds the user by app_user_id and updates subscription status (e.g. set subscription_status = 'active').
  4. The app can then poll your backend or use your own logic to show premium content. Do not grant access only from the client; the webhook is the source of truth.

Relevant for this package: the webhook payload’s user identifier is the same app_user_id you get from userIdentity() and pass as external_id to RevenueCat. Use it to match events to your users table. See RevenueCat webhooks for payload shape and setup.


Main ID + aliases

You get a single main appUserId and an optional list of aliases (other IDs linked to the same user). Use the main ID for RevenueCat, OneSignal, backend, etc. Use aliases for fraud checks or when your backend merges users:

const result = await userIdentity();
// result.appUserId = main/canonical ID
// result.aliases = ['old_install_id', 'merged_user_id']

// When backend says "merge ID X into this user":
await linkAlias('old_install_id');

Web vs native

  • Native Despia app (iOS/Android): await userIdentity() returns { appUserId, installId, source, aliases }. Use it.
  • Web (browser): await userIdentity() returns null. The package does nothing. Use your own web identity (session, cookies, etc.).
  • Check before use: Always check if (result) or if (isDespia()) before syncing to RevenueCat, OneSignal, or backend.

Platform detection: Despia sets the user agent so you can detect native runtime and platform. Official guide: User Agent (no package required; use navigator.userAgent.toLowerCase().includes('despia') and check for iphone/ipad/android). When you use this package, use its helpers instead of reimplementing:

import { isDespia, getPlatform } from '@despia/user-identity';

const inDespia = isDespia();           // same as userAgent.includes('despia')
const platform = getPlatform();       // 'ios' | 'android' | null (iphone/ipad vs android)
const isDespiaIOS = inDespia && platform === 'ios';
const isDespiaAndroid = inDespia && platform === 'android';
// e.g. show RevenueCat in Despia, Stripe on web

Same behavior as the official doc: isDespia() checks for "despia"; getPlatform() returns 'ios' (iphone/ipad) or 'android'.


Common scenarios

| Scenario | What happens | |----------|--------------| | First install, no backup | source: 'new', appUserId = install ID | | Reinstall, iCloud/Android backup restored | source: 'vault', appUserId from backup | | Reinstall, no backup, but had purchases | source: 'restore', appUserId from purchase history | | New device, same Apple/Google account, had purchases | source: 'restore' if purchase history syncs, else source: 'new' | | User taps "Restore purchases" | restore() returns recovered appUserId if any purchase had externalUserId |

Important: For restore to work, you must pass app_user_id as external_id when launching RevenueCat paywalls. Otherwise purchases aren’t linked and recovery won’t find them.


Troubleshooting

| Issue | Likely cause | |-------|---------------| | getPlatform is undefined / not in package | Use @despia/[email protected] or later. Import: import { getPlatform } from '@despia/user-identity'. Do not use a custom detectPlatform helper. | | Calls seem to hang / no callback in native | This package uses Despia’s promises directly—no extra timeout. Data is processed as soon as the native layer resolves (vault has a 30s timeout on the native side). Vault and install-ID fetch run in parallel. Install ID uses window.uuid first (set by native before any package loads), then despia.uuid, then the async get-uuid call only if needed. | | Getting $RCAnonymous instead of real ID | Recovery from purchase history prefers non–RevenueCat-anonymous externalUserId (ignores $RCAnonymous when a real ID exists). Ensure paywalls are launched with your app_user_id as external_id so RevenueCat stores it. | | userIdentity() returns null | Running in browser, not Despia native app. Use isDespia() to check. | | restore() returns null | No purchases, or purchases weren’t made with external_id (RevenueCat). Always pass app_user_id as external_id when launching paywalls. | | User “lost” after reinstall | iCloud/Android backup may not have synced yet, or backup was disabled. Restore purchases can still recover if they had a purchase with externalUserId. |


API & response shapes

userIdentity() (alias: getAppUserId())

Resolves identity and persists to vault. Call once at app launch.

Returns: IdentityResult | null

| Field | Type | Description | |-------|------|-------------| | appUserId | string | Main user ID. Use for RevenueCat external_id, OneSignal, backend. | | installId | string | Device/install UUID from get-uuid://. Changes per install. | | source | 'vault' \| 'restore' \| 'new' | Where the ID came from: vault (iCloud/backup), restore (purchase history), or new (first install). | | aliases | string[] | Alternate IDs linked via linkAlias(). For fraud checks, analytics. |

Returns null when not in Despia native runtime (e.g. web).

// Example
const result = await userIdentity();
// { appUserId: "abc-123", installId: "device-xyz", source: "vault", aliases: [] }

restore()

Recovers identity from purchase history. Use for "Restore purchases" button.

Returns: { appUserId: string; aliases: string[] } | null

| Field | Type | Description | |-------|------|-------------| | appUserId | string | Recovered ID from a purchase with externalUserId. Sync to services. | | aliases | string[] | Current aliases (from vault). |

Returns null when no purchases found, no purchase has externalUserId, or not in Despia.

// Example
const restored = await restore();
// { appUserId: "abc-123", aliases: [] }  or  null

setAppUserId(appUserId: string)

Sets the main ID (e.g. from login). Persists to vault. No return value.


linkAlias(aliasId: string)

Links an alternate ID to this user. Persists to vault. No return value. Skips if aliasId is already main or in aliases.


isDespia()

Returns: boolean - true if navigator.userAgent includes "despia" (native app), else false.


getPlatform()

Returns: 'ios' | 'android' | null - platform when in Despia (based on user agent: iphone/ipad vs android). null on web or unknown.


API summary

| Function | Returns | |----------|---------| | userIdentity() | { appUserId, installId, source, aliases } \| null | | restore() | { appUserId, aliases } \| null | | setAppUserId(id) | void | | linkAlias(id) | void | | isDespia() | boolean | | getPlatform() | 'ios' \| 'android' \| null |