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 via the refresh cookie 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()
await SendoraCloud.track("paywall.shown");
await SendoraCloud.track("paywall.dismissed");

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

await SendoraCloud.track("signup.completed");

Calling signUp() on a brand-new install (no anonymous session yet) creates a fresh user. Either way, the SDK reads the refresh-token cookie as the authoritative signal — never falls back to a stale isAnonymous flag.

When the email already exists

If the email is already registered, signUp() throws EmailAlreadyTakenError (backend EMAIL_ALREADY_TAKEN) without consuming the anonymous refresh token. Your UI should fall back to signIn() — but be aware: signIn() wipes the local anonymous identity, so every event tracked while anonymous stays attributed to the now-orphaned anonymous user_id on the backend. It does NOT auto-transfer to the signed-in account.

To preserve those events, grab the anonymous refresh token before calling signIn(), then call auth.merge(anonRefresh) after the sign-in succeeds. merge() reattributes every event from the anonymous row to the now-signed-in user_id and deletes the anonymous row. Same semantics as Firebase's manual account-link transfer + Auth0's accounts.link.

async function signUpOrSignIn(email: string, password: string) {
  try {
    // Happy path: same user_id preserved via /auth-service/upgrade
    const { user } = await SendoraCloud.auth.signUp({ email, password });
    return user;
  } catch (err) {
    if (!(err instanceof EmailAlreadyTakenError)) throw err;

    // Email already registered. We need to preserve anonymous events
    // across the sign-in boundary — capture anon refresh BEFORE
    // signIn() wipes it.
    const anonRefresh = await SendoraCloud.auth.getRefreshToken();

    const { user } = await SendoraCloud.auth.signIn(email, password);

    // Best-effort merge — silent-fail-tolerant (anon row may be gone
    // if signIn() was retried, network blip, etc.). Background-safe;
    // the user is already signed in by this point.
    if (anonRefresh) {
      SendoraCloud.auth
        .merge(anonRefresh)
        .catch(() => { /* anon already drained; no data loss possible */ });
    }
    return user;
  }
}

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/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