Rovenue SDK
Getting Started

Core Concepts

The mental model behind the Rovenue SDK — two IDs, cache-first reads, the change stream, and how purchases flow.

The Rovenue SDK is built around a small set of concepts that apply identically across React Native, Swift, and Kotlin. Understanding them makes every other part of the API predictable.


The two user IDs: rovenueId and appUserId

Every install gets a permanent, anonymous rovenueId (prefix rov_) the moment configure() is called. It is stable for the lifetime of the app install — it does not rotate on app restart, network change, or OS update.

At login, you call identify(appUserId) to attach your own backend user ID to the rovenueId. From that point, Rovenue uses the appUserId as the canonical subscriber identity.

Install                   Login                       Sign-out
  │                          │                            │
  ▼                          ▼                            ▼
rovenueId = rov_abc...   appUserId = "usr_123"        logOut()
(anonymous subscriber)   (identified subscriber)      (new rovenueId minted)

The current state is always available via currentUser():

Types on this page

type User = {
  rovenueId: string;        // permanent per-install id, prefix rov_; never changes
  appUserId: string | null; // your backend user id; null until identify()
};

type ChangeEvent =
  | 'EntitlementsChanged'      // active entitlements updated
  | 'IdentityChanged'          // rovenueId or appUserId changed (identify / logOut)
  | 'VirtualCurrenciesChanged'; // virtual-currency balances changed
FieldTypeDescription
rovenueIdstringPermanent per-install identifier. Never reassigned for the life of the install.
appUserIdstring | nullYour backend's user ID. null in anonymous mode. Set by identify(); cleared by logOut().

Swift uses User.rovenueId / User.appUserId. Kotlin mirrors the same field names. ChangeEvent is a Swift enum (.entitlementsChanged) and a Kotlin enum (ENTITLEMENTS_CHANGED). See Types.

The rovenueId is the unit of billing: it is what gets linked to App Store / Play Store receipts and what holds virtual-currency balances. Even in anonymous mode, purchases work correctly — they attach to the rovenueId and are transferred to the appUserId automatically when identify() is called.

import { Rovenue, useCurrentUser } from '@rovenue/react-native-sdk';

// Imperative read
const user = Rovenue.currentUser();
// user.rovenueId  → "rov_abc123..."
// user.appUserId  → null (anonymous) or "usr_123" (identified)

// Reactive hook (re-renders on IdentityChanged)
function Header() {
  const user = useCurrentUser(); // User | null
  return <Text>{user?.appUserId ?? user?.rovenueId}</Text>;
}
import Rovenue

let user = await Rovenue.shared.currentUser()
// user.rovenueId  → "rov_abc123..."
// user.appUserId  → nil (anonymous) or "usr_123" (identified)
import dev.rovenue.sdk.Rovenue

val user = Rovenue.shared.currentUser()
// user.rovenueId  → "rov_abc123..."
// user.appUserId  → null (anonymous) or "usr_123" (identified)

identify() — binding a logged-in user

Call identify(appUserId) once your auth flow completes and you have a stable, permanent user ID. The call is optimistic: local state (including currentUser()) updates synchronously; in the background the SDK calls POST /v1/identify to perform the server-side bind and purchase transfer.

Pass a stable, permanent user ID — a UUID or database primary key from your backend. Never pass a session token, email address, or anything that can change. Passing an unstable ID creates a new subscriber record per login and the merge cannot be undone. See Identifying users and Identity & consent.

logOut() — clearing identity on sign-out

logOut() mints a fresh rovenueId and clears the appUserId. All scope-bound caches (entitlements, credits, offerings) are cleared. The next user on a shared device starts completely clean. No network call is needed — the operation is local-only. Session events and attribute changes the outgoing user made but the SDK has not sent yet are kept, not discarded: each was recorded under that user's rovenueId and is still delivered under it, never under the next user.

Always call logOut() on sign-out, especially on shared devices (family iPad, demo devices). Failing to do so leaves the previous user's entitlements accessible to the next session.

Method references: identify, logOut, currentUser.


Entitlements — what your app actually checks

An entitlement is a named access grant your app checks at runtime (for example, "pro", "premium", or "credits_pack"). Entitlements are independent of store SKUs: you configure the mapping in the Rovenue dashboard, then check the entitlement ID in code.

The key mental model: your feature-gating code never references a product ID or SKU. It only ever asks "does the current user have entitlement "pro"?" The Rovenue dashboard is where you wire store products to entitlements. This means you can add new products, change prices, or run a promo without shipping a new binary.

Entitlements have an isActive boolean — the only field your app needs at the gate:

type Entitlement = {
  id: string;                // entitlement id as configured in the dashboard
  isActive: boolean;         // true = access currently granted
  productIdentifier: string; // store SKU that created the grant
  store: string;             // "app_store" | "play_store" | "stripe"
  expiresIso?: string;       // ISO-8601 expiry; undefined for lifetime products
};
FieldTypeDescription
idstringDashboard entitlement ID. This is what you pass to entitlement().
isActivebooleanThe only field to check at the gate.
productIdentifierstringThe store SKU that created the grant.
storestringWhich store issued the grant.
expiresIsostring?Expiry in ISO-8601. Absent for lifetime products.

See Types and Entitlements guide.

Check isActive, not expiresIso. An entitlement with a future expiry date and isActive: false is in a grace period or was revoked. The server sets isActive based on subscription state — your app should treat it as a boolean gate.

import { useEntitlement, Rovenue } from '@rovenue/react-native-sdk';

// Reactive hook — re-renders on EntitlementsChanged
function ProFeature() {
  const pro = useEntitlement('pro');
  if (!pro?.isActive) return <Paywall />;
  return <TheActualFeature />;
}

// Imperative — for non-component code
const pro = Rovenue.entitlement('pro');
const all = Rovenue.entitlementsAll();
import Rovenue

// Cache-first read; never suspends on network
let pro = await Rovenue.shared.entitlement("pro")
let all = await Rovenue.shared.entitlementsAll()

if pro?.isActive == true {
  showProFeature()
}
import dev.rovenue.sdk.Rovenue

val pro = Rovenue.shared.entitlement("pro")
val all = Rovenue.shared.entitlementsAll()

if (pro?.isActive == true) {
  showProFeature()
}

Virtual Currencies — multi-currency balances

Virtual currencies let you grant any number of named currency balances per subscriber (e.g. COINS, GEMS, ENERGY). The SDK exposes them as cache-first reads. Spending is server-side only — your backend calls the secret-key POST /v1/virtual-currencies/:appUserId/:code/transactions endpoint; there is no client-side spend method.

// Read all balances (cache-first) — returns { [currencyCode]: amount }
Rovenue.virtualCurrencies()

// Read a single currency by code (0 if absent)
Rovenue.virtualCurrency('COINS')

// Force a network refresh
Rovenue.refreshVirtualCurrencies()

React Native also ships useVirtualCurrencies() → Record<string, number> and useVirtualCurrency(code) → number hooks that subscribe to VirtualCurrenciesChanged internally and re-render automatically.

Virtual-currency balances are part of the PurchaseResult — after a currency-pack purchase, result.virtualCurrencies already reflects the updated balances. No separate refresh is needed. See the Virtual Currencies guide.

When a product grants currency

A product's currency grant is set to fire on PURCHASE, on RENEWAL, or on BOTH. Rovenue applies one rule on every store:

  1. Only a paid term grants. A paid term is one store charge with an amount above zero: the first charge of a subscription, a renewal, the first paid charge after a free trial, a resubscribe after the old subscription lapsed, or a one-time purchase.
  2. Each term grants once per currency, project-wide. A redelivered notification, a receipt the app sends again, a restore, or a merge of two subscribers between two deliveries does not grant a second time.
  3. Nothing else grants. Free trials, auto-renew toggles, cancellations, expiries, billing issues, grace periods, plan-change announcements, family-shared members and operator comps (amount zero) grant nothing.
  4. A refund takes back that term's grant. The refunded term's grants come back out of the balance, in proportion for a partial refund. Rovenue never takes more than the live balance holds. A revoke takes back every term of the subscription. If the store reverses the refund, the grant is restored.

The first paid term of a subscription fires PURCHASE; every later auto-renewed term fires RENEWAL. BOTH fires on either. A resubscribe fires PURCHASE; a currency with no PURCHASE grant gets its RENEWAL grant instead — never both.

EventGrantsTrigger
Buy (first paid charge, or a one-time purchase)yesPURCHASE
RenewalyesRENEWAL
Free trial startsno—
First paid charge after a free trialyesPURCHASE
Resubscribe after the subscription expired (any store)yesPURCHASE, else RENEWAL
Cancel (now or at period end), or undo a cancelno; the balance is kept—
Refund of one term (full or partial)takes back that term's grant (in proportion)—
Revoke (family sharing removed, chargeback)takes back every term's grant—

Offerings and packages — the product catalogue

An offering is a named, versioned slice of your product catalogue as configured in the Rovenue dashboard. The current offering is what your funnels sell this user (you can A/B test offerings with experiments). Each offering contains packages — individual purchasable items.

type Offerings = {
  current: Offering | null;
  all: Record<string, Offering>;
};

type Offering = { identifier: string; isDefault: boolean; packages: Package[] };
type Package = {
  identifier: string;
  packageType: 'monthly' | 'annual' | 'weekly' | /* ... */ 'lifetime';
  product: StoreProduct;
};
type StoreProduct = {
  id: string;
  type: 'subscription' | 'consumable' | 'non_consumable';
  productCategory: 'subscription' | 'nonSubscription';
  displayName: string;
  description: string | null;
  priceString: string | null; // localised, e.g. "$4.99"
  price: number | null;
  currencyCode: string | null;
  subscriptionPeriod: Period | null; // { value: number; unit: 'day'|'week'|'month'|'year'; iso8601: string }
  introPrice: IntroPrice | null; // iOS only
  discounts: Discount[]; // iOS promotional offers
  isEligibleForIntroOffer: boolean | null;
  subscriptionOptions: SubscriptionOption[] | null; // Android only
  defaultOption: SubscriptionOption | null;
  pricePerMonth: number | null; // approximated per-unit prices
  pricePerMonthString: string | null;
  // ... see Types reference for full schema
};

getOfferings() returns a cached result and refreshes in the background. When you need a guaranteed-fresh list (unusual; typically not needed on the critical path), call refreshEntitlements() and then getOfferings().

Funnels — onboarding and payment screen in one document

A funnel is the one authoring product for everything before the purchase: a multi-screen onboarding with branching, values, and the payment screen in a single document, created for exactly one target — web (hosted URL, Stripe checkout) or native (served to a placement inside your app, store purchase). The SDK presents native funnels and reports their lifecycle events; see Funnels for the full guide.


Cache-first reads and background sync

All read operations — entitlement(), entitlementsAll(), virtualCurrencies(), virtualCurrency(), getOfferings() — return instantly from an on-device SQLite cache powered by the Rust core. They never block on a network call.

The SDK keeps the cache warm automatically:

  1. On configure(): loads the SQLite snapshot, serves it immediately.
  2. While foregrounded: polls the server on a configurable interval (default: 30 s) and applies deltas silently.
  3. On purchase: immediately calls refreshEntitlements() before resolving purchase(), so PurchaseResult is always authoritative.
  4. On setForeground(true): forces an immediate refresh (call this from your app's foreground lifecycle event).

The cache is backed by rusqlite — it persists across app restarts. On first launch (empty cache) entitlements are null / empty until the first network poll completes. Design your paywall to handle this gracefully (show a loading state, not a blank screen).

import { Rovenue } from '@rovenue/react-native-sdk';
import { AppState } from 'react-native';

// Tell the SDK when the app foregrounds so it refreshes immediately
AppState.addEventListener('change', state => {
  Rovenue.setForeground(state === 'active');
});
import Rovenue
import UIKit

// In SceneDelegate or AppDelegate:
func sceneDidBecomeActive(_ scene: UIScene) {
  Rovenue.shared.setForeground(true)
}

func sceneWillResignActive(_ scene: UIScene) {
  Rovenue.shared.setForeground(false)
}
import dev.rovenue.sdk.Rovenue
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.ProcessLifecycleOwner

ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
  override fun onStart(owner: LifecycleOwner) = Rovenue.shared.setForeground(true)
  override fun onStop(owner: LifecycleOwner) = Rovenue.shared.setForeground(false)
})

See App lifecycle for the full setup.


The change stream — reacting to updates

When the SDK receives updated data from the server, it emits a ChangeEvent. Subscribe once and update your UI reactively — no polling, no manual refresh.

type ChangeEvent =
  | 'EntitlementsChanged'      // active entitlements updated
  | 'IdentityChanged'          // rovenueId or appUserId changed
  | 'VirtualCurrenciesChanged'; // virtual-currency balances changed
EventWhen it fires
EntitlementsChangedAfter purchase, restore, server-side revocation, or background poll.
IdentityChangedAfter identify() or logOut().
VirtualCurrenciesChangedAfter a purchase that grants virtual currency or a server-side spend/grant.
import { Rovenue } from '@rovenue/react-native-sdk';
import { useEffect } from 'react';

function useRovenueChanges() {
  useEffect(() => {
    const unsubscribe = Rovenue.addChangeListener(event => {
      if (event === 'EntitlementsChanged') {
        console.log('Entitlements updated');
      }
      if (event === 'VirtualCurrenciesChanged') {
        console.log('New balances:', Rovenue.virtualCurrencies());
      }
    });
    return unsubscribe; // cleans up on unmount
  }, []);
}

In practice, using useEntitlement() and useVirtualCurrencies() hooks is simpler — they subscribe to the change stream internally and re-render automatically.

Swift exposes ChangeEvent as a typed enum and the stream as AsyncStream<ChangeEvent>:

import Rovenue

Task {
  for await event in Rovenue.shared.changes {
    switch event {
    case .entitlementsChanged:
      // read new entitlements
    case .virtualCurrenciesChanged:
      let balances = await Rovenue.shared.virtualCurrencyBalances()
      print("New balances:", balances)
    case .identityChanged:
      let user = await Rovenue.shared.currentUser()
      print("Now identified as:", user.appUserId ?? "anonymous")
    }
  }
}

Kotlin exposes ChangeEvent as a typed enum and the stream as SharedFlow<ChangeEvent>:

import dev.rovenue.sdk.Rovenue
import dev.rovenue.sdk.ChangeEvent

viewModelScope.launch {
  Rovenue.shared.changes.collect { event ->
    when (event) {
      ChangeEvent.ENTITLEMENTS_CHANGED -> {
        // read new entitlements
      }
      ChangeEvent.VIRTUAL_CURRENCIES_CHANGED -> {
        val balances = Rovenue.shared.virtualCurrencyBalances()
        println("New balances: $balances")
      }
      ChangeEvent.IDENTITY_CHANGED -> {
        val user = Rovenue.shared.currentUser()
        println("Now identified as: ${user.appUserId ?: "anonymous"}")
      }
    }
  }
}

See Reacting to changes for patterns including Combine publishers, SwiftUI .task modifiers, and Compose collectAsState.


logOut() — the shared-device contract

When a user signs out, call logOut(). This:

  1. Mints a fresh rovenueId (the old one is preserved on the server, attached to the appUserId).
  2. Clears appUserId from local state.
  3. Clears all scope-bound caches (entitlements, virtual currencies, offerings).
  4. Emits IdentityChanged.

The next user who opens the app starts in a clean anonymous session. Their subsequent identify() call attaches them to their own subscriber record.

Skipping logOut() on a shared device means the next user sees the previous user's entitlements until the background poll overwrites the cache — up to 30 seconds. Always call it on sign-out.

Method reference: logOut.


Further reading: Quick Start for runnable end-to-end code. Identifying users for the full identify() / logOut() flow. Entitlements for the subscription state machine. Reacting to changes for reactive UI patterns. Methods and Types for the full API reference.

On this page