Skip to content
Sendora Cloud
Create account
Get started — React Native

First 5 minutes with React Native.

This page covers the React-Native–specific gotchas that the generic quickstart doesn't surface — Hermes CSPRNG polyfill, anonymous-first identity, push token receipts, and the loop-closing demo send.

1. Install

One npm command — the SDK auto-installs everything it needs.

npx expo install @sendoracloud/sdk-react-native @react-native-async-storage/async-storage

Since 0.17.0 the SDK ships react-native-get-random-values as a hard dependency and auto-applies the polyfill at SDK load. No manual import "react-native-get-random-values" in your entry file required. (Sendora refuses to mint anonymous IDs from Math.random — predictable IDs are a security floor regression. The polyfill makes secure random the default.)

Upgrading from 0.16.x? You can delete the manual import "react-native-get-random-values" line from your index.js / index.tsx — it's now a no-op. Leaving it in does no harm.

2. Pick the right runner — Expo Go vs Dev Client

If push notifications matter to you, you must move off plain Expo Go. Expo dropped remote-push support in Expo Go starting SDK 53 — the SDK's registerPushToken() will return null in that runtime. Auth, analytics, and warm deep links still work fine in Expo Go.

FeatureExpo Go (SDK 53+)Dev Client / EASBare RN
Auth (sign-up, MFA, passkeys)✅✅✅
Analytics (track / screen / identify)✅✅✅
Deep links (warm path)✅✅✅
Push token registration❌✅✅
Play Install Referrer (Android)❌✅✅

Switching from Expo Go to a Dev Client is a one-time setup — after the first build your day-to-day npx expo start DX is identical.

# One-time: generate native projects + build the dev client
npx expo prebuild --clean
npx expo run:ios          # or: npx expo run:android

# Day-to-day: same as Expo Go
npx expo start --dev-client

3. Initialise the SDK

Call init() once on app startup. The public key belongs in SENDORA_PUBLIC_KEY (or your env-var of choice — Sendora doesn't require a specific name). Mint one in Dashboard → Settings → API Keys → Create key → Public.

import SendoraCloud from "@sendoracloud/sdk-react-native";

await SendoraCloud.init({
  apiKey: process.env.EXPO_PUBLIC_SENDORA_PUBLIC_KEY!,
});

// Anonymous user is now created. Track immediately.
await SendoraCloud.track("app.opened");

4. Anonymous-first identity

Sendora creates an anonymous user the first time init() runs. Every subsequent track() + identify() attaches to the same user_id until you upgrade.

Critical: when an anonymous user later signs up with email + password, call signUp() on the existing session. Sendora detects the anonymous session from the stored refresh token and upgrades the same user_id in place — your analytics + funnels stay continuous across the anon-to-known boundary.

// Anonymous user — works immediately after init()
SendoraCloud.track("paywall.shown");
SendoraCloud.track("paywall.dismissed");

// User decides to sign up. SAME user_id is preserved.
const user = await SendoraCloud.auth.signUp(
  "alice@example.com",
  "correct-horse-battery-staple",
);
// user.id === the anonymous user_id from before. No funnel break.

SendoraCloud.track("signup.completed");

Calling signUp() on a brand-new install (no anonymous session yet) creates a fresh user. Either way, the SDK treats the stored session as the authoritative signal — never a stale isAnonymous flag.

The anonymous mint is retry-safe: POST /auth-service/anonymous accepts an optional per-attempt mintKey (a random opaque the client keeps in ordinary storage until it holds the tokens), and a retry within 24 h carrying the same key returns the same guest with a fresh token pair instead of minting a second account when the first response was lost. It is stored only as a hash and is not a device credential — it expires after 24 h and never matches an account that has since been upgraded. The SDKs send it automatically from their next release; if you call the endpoint yourself, generate a fresh key per sign-in attempt.

When the email already exists

If the email is already registered, signUp() throws EmailAlreadyTakenError (backend EMAIL_ALREADY_TAKEN) without consuming the anonymous session. Your UI should fall back to signIn(). Since SDK 1.41 a sign-in holds the guest on the device instead of deleting it: the guest stays alive on the server and appears in auth.getHeldGuests(), and its events stay on its own user_id until you decide.

To carry those events over, call auth.claimHeldGuest(guest.userId) after the sign-in succeeds. It reattributes the guest's events and profile to the signed-in user_id, deletes the guest row and fires auth.user_merged. restoreHeldGuest() switches back to the guest instead, and discardHeldGuest() deletes it. Same semantics as Firebase's manual account-link transfer + Auth0's accounts.link.

async function signUpOrSignIn(email: string, password: string) {
  try {
    // Happy path: the guest is promoted in place — same user_id.
    return await SendoraCloud.auth.signUp(email, password);
  } catch (err) {
    if (!(err instanceof EmailAlreadyTakenError)) throw err;

    // Email already registered. signIn() HOLDS the guest on this device —
    // nothing is deleted — so its events can still be carried over.
    const result = await SendoraCloud.auth.signIn(email, password);
    if ("mfaRequired" in result) throw new Error("finish MFA first");

    const [guest] = SendoraCloud.auth.getHeldGuests();
    if (guest) {
      // Moves the guest's events + profile onto this account, deletes the
      // guest row, fires auth.user_merged. On failure the guest stays held:
      // retry later, nothing is lost.
      await SendoraCloud.auth.claimHeldGuest(guest.userId).catch(() => {});
    }
    return result;
  }
}

Why this is manual: auto-merging on everysignIn() would silently combine an anonymous browser session with a different person's real account — common when two people share a device. Forcing the merge call into your code keeps the data-mixing decision explicit. Apps that want the auto behaviour can wrap signIn() with the snippet above as a thin helper.

5. Register a push token (requires Dev Client)

Pair Sendora's registerPushToken() with your push provider (Firebase Messaging on Android, APNs on iOS). The call returns a PushTokenReceipt so you can log a confirmation:

import messaging from "@react-native-firebase/messaging";

const fcmToken = await messaging().getToken();

const receipt = await SendoraCloud.registerPushToken({
  token: fcmToken,
  platform: Platform.OS === "ios" ? "ios" : "android",
  bundleId: "com.yourapp.production",
  // bundleId is optional but strongly recommended for multi-env apps
  // so dev / staging tokens never get a prod APNs / FCM push.
});

console.log("Sendora token id:", receipt.tokenId, "new?", receipt.created);

6. Send a test push (close the loop)

Tokens registered? Confirm end-to-end by sending one to your own device from the dashboard or directly via the API:

curl -X POST https://api.sendoracloud.com/api/v1/orgs/<ORG_ID>/push/send \
  -H "x-api-key: $SENDORA_SECRET_KEY" \
  -d '{
    "userIds": ["<the user_id you signed up above>"],
    "title": "Hello from Sendora",
    "body": "Push works ✅",
    "data": { "kind": "test" }
  }'

From the dashboard: Messaging → Push → Send. The recipient picker accepts a single user id for one-off testing without setting up an audience.

Env-var naming

Sendora's SDKs accept any string in apiKey, but the canonical convention across docs + examples is:

  • SENDORA_PUBLIC_KEY — public key (pk_…), ships in browser / mobile bundles. Safe to commit to a public repo only when project-scoped.
  • SENDORA_SECRET_KEY — secret key (sk_…), server-side only. Never include in any client bundle.
  • In Next.js, the browser-side public key needs the NEXT_PUBLIC_ prefix: NEXT_PUBLIC_SENDORA_PUBLIC_KEY.
  • Older docs referenced NEXT_PUBLIC_SENDORA_KEY / SENDORA_API_KEY. Both still work — the SDK only cares about the value, not the env-var name — but new code should use the canonical names above.

SDK stability commitment

@sendoracloud/sdk-react-native is currently on the 0.10.x line. Pre-1.0 minor bumps may include breaking changes; patch bumps (0.10.x → 0.10.y) are backwards-compatible. The next major (1.0) is targeted once the HttpOnly-cookie SSR auth + auto-trait extraction features have soaked in production for two consecutive months without a schema-shape change. Once 1.0 ships, semver is strict — no breaking changes outside major bumps.

Next steps