Amba

React Native SDK

React Native quickstart for @layers/amba-react-native — install, configure, session storage, push registration, Apple/Google sign-in. Bare-RN-first; Expo flavors shown as sub-tabs.

@layers/amba-react-native is the React Native SDK. Same API as every other Amba SDK; with a secure token store installed, users stay signed in across app restarts (see Where session tokens are stored).

This page is bare React Native first — every snippet uses non-Expo libraries by default, with the Expo equivalent as a sub-tab. If you're on the Expo managed workflow, use @layers/amba-expo instead — same runtime, plus a config plugin that wires native config at prebuild time.

1. Install

Core package (both flavors):

pnpm add @layers/amba-react-native @react-native-async-storage/async-storage \
         react-native-keychain react-native-config
npx pod-install   # iOS only — links the storage and Keychain native modules

Peer libraries for Apple/Google sign-in and push:

pnpm add @invertase/react-native-apple-authentication \
         @react-native-google-signin/google-signin \
         @notifee/react-native \
         react-native-permissions
# iOS: native modules need pod install + Apple Sign In capability in Xcode.
npx pod-install

The runtime call into @layers/amba-react-native is identical either way — once you have an identity token (Apple) or an id token (Google) or a push token (APNs/FCM), pass it to Amba.auth.signInWithApple(identityToken) / Amba.auth.signInWithGoogle(idToken) / Amba.push.register(...) unchanged. Only the producer library differs. When the sign-in request carried a nonce, pass the raw value too: Amba.auth.signInWithApple(identityToken, { nonce }) (see sign-in nonce).

Crypto setup

Required. Hermes (React Native's default JS engine) does not ship crypto.getRandomValues. Amba uses it to generate identifiers that need to be unpredictable, such as the anonymous id that signs an anonymous user back in. Without the polyfill, Amba.configure() throws SecureRandomUnavailableError (from @layers/amba-react-native 4.0.14).

Install the polyfill:

pnpm add react-native-get-random-values
npx pod-install   # iOS only

Then import it once, as the very first import in your app's entry file, before any other Amba imports:

// index.js or App.tsx — must be the FIRST import
import 'react-native-get-random-values';
import { Amba } from '@layers/amba-react-native';

Order matters. The polyfill installs crypto.getRandomValues on the global object as a side effect of being imported; if Amba loads first, it captures the (missing) global and the polyfill never takes effect. Put the import on line 1.

A one-shot console warning fires at runtime if Amba sees the fallback path engaged — that's your signal the polyfill import is missing or out of order.

Metro config for monorepos

If you consume @layers/amba-react-native via pnpm/yarn workspaces, point Metro at the workspace root so it can resolve hoisted dependencies:

// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const path = require('path');
 
const workspaceRoot = path.resolve(__dirname, '..', '..');
 
const config = {
  watchFolders: [workspaceRoot],
  resolver: {
    nodeModulesPaths: [
      path.resolve(__dirname, 'node_modules'),
      path.resolve(workspaceRoot, 'node_modules'),
    ],
  },
};
 
module.exports = mergeConfig(getDefaultConfig(__dirname), config);

Without watchFolders + resolver.nodeModulesPaths, Metro only looks in this package's local node_modules and fails with "Unable to resolve module @layers/amba-react-native".

2. Configure on app boot

// App.tsx
import { useEffect } from 'react';
import Config from 'react-native-config'; // pnpm add react-native-config
import { Amba } from '@layers/amba-react-native';
import { RootNavigator } from './navigation';
 
export default function App() {
  useEffect(() => {
    void Amba.configure({
      projectId: Config.AMBA_PROJECT_ID!, // from .env via react-native-config
      clientKey: Config.AMBA_CLIENT_KEY!,
      // Optional:
      // apiUrl: 'https://api.amba.dev', // default
      // debug: __DEV__,
    });
  }, []);
 
  return <RootNavigator />;
}

Put your credentials in a .env file at the project root:

AMBA_PROJECT_ID=<your project id>
AMBA_CLIENT_KEY=<your client key>

react-native-config injects them at build time into Config.AMBA_PROJECT_ID / Config.AMBA_CLIENT_KEY (no process.env indirection needed). See the react-native-config setup guide for the iOS / Android native config wiring (one-time).

Amba.configure() is idempotent — calling it again with the same key is a no-op.

Where session tokens are stored

The session and refresh tokens go wherever the tokenStorage option says. The anonymous id and user id stay in AsyncStorage in every mode.

tokenStorageTokens live inAfter a restart
"secure" (default)The iOS Keychain / Android Keystore, through secureStorage when you pass one, otherwise react-native-keychain when it is installed. With neither, memory only, plus one warning.Signed in with a secure store, signed out without one
"memory"Memory onlySigned out
"asyncStorage"Plain AsyncStorage, which anyone who can read the app's files or an unencrypted backup can copySigned in

Install react-native-keychain (as above) to keep users signed in across launches. To use another keystore-backed module, pass an adapter with getItem, setItem and removeItem:

import * as SecureStore from 'expo-secure-store';
 
await Amba.configure({
  projectId,
  clientKey,
  secureStorage: {
    getItem: (key) => SecureStore.getItemAsync(key),
    setItem: (key, value) => SecureStore.setItemAsync(key, value),
    removeItem: (key) => SecureStore.deleteItemAsync(key),
  },
});

Choose tokenStorage: "asyncStorage" only when no secure store can be installed and signing in again on every launch is unacceptable: a stolen backup then carries a session that stays usable for 30 days and refreshable for 90. Tokens an earlier SDK version left in AsyncStorage move into the secure store on the first launch after the upgrade (or stay in memory for that launch when there is none), and the plaintext copy is removed. If sign-out can't clear the secure store, the SDK starts a new anonymous id so the stored session can't come back.

While the secure store can't be read (an iOS device that has not been unlocked since it restarted), a call that would replace the stored session with a new anonymous user fails with SECURE_STORE_UNAVAILABLE and leaves that session in place; retry once the device is unlocked. When a launch finds no tokens for a session that had signed in or linked an identity (memory storage, or a secure-store entry that is gone), the device starts a new anonymous id, as a sign-out would.

3. First event + first sign-in

Amba.events.track is authenticated server-side. Mint an anonymous session before the first track call:

import { Amba } from '@layers/amba-react-native';
 
// 1. Sign in first — minted anonymously on first launch, restored on
//    later launches when a secure token store is installed.
const session = await Amba.auth.signInAnonymously();
console.log('signed in as', session.user.id);
 
// 2. Track now happens with a valid session token.
await Amba.events.track('app_opened');

Identical code on bare RN and Expo — @layers/amba-react-native is the only import either flavor needs at this layer. The session is restored on later launches when a secure token store is installed, as described in Where session tokens are stored.

4. Sign in with Apple

import { appleAuth, AppleButton } from '@invertase/react-native-apple-authentication';
import { Amba } from '@layers/amba-react-native';
import { Platform, View } from 'react-native';
 
export function SignInWithApple() {
  if (Platform.OS !== 'ios') return null;
  return (
    <AppleButton
      buttonStyle={AppleButton.Style.BLACK}
      buttonType={AppleButton.Type.SIGN_IN}
      style={{ width: '100%', height: 48 }}
      onPress={async () => {
        const response = await appleAuth.performRequest({
          requestedOperation: appleAuth.Operation.LOGIN,
          requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
        });
        if (response.identityToken) {
          await Amba.auth.signInWithApple(response.identityToken);
        }
      }}
    />
  );
}

Add the Sign In with Apple capability in Xcode (Signing & Capabilities → + Capability → Sign In with Apple). One-time per target.

5. Sign in with Google

import { useEffect } from 'react';
import { Button } from 'react-native';
import {
  GoogleSignin,
  GoogleSigninButton,
  statusCodes,
} from '@react-native-google-signin/google-signin';
import { Amba } from '@layers/amba-react-native';
 
GoogleSignin.configure({
  webClientId: 'YOUR_GOOGLE_OAUTH_WEB_CLIENT_ID',
  iosClientId: 'YOUR_GOOGLE_OAUTH_IOS_CLIENT_ID',
});
 
export function GoogleButton() {
  return (
    <GoogleSigninButton
      size={GoogleSigninButton.Size.Wide}
      color={GoogleSigninButton.Color.Dark}
      onPress={async () => {
        try {
          await GoogleSignin.hasPlayServices();
          const { idToken } = await GoogleSignin.signIn();
          if (idToken) {
            await Amba.auth.signInWithGoogle(idToken);
          }
        } catch (error: unknown) {
          if ((error as { code?: string }).code === statusCodes.SIGN_IN_CANCELLED) {
            return; // user cancelled — silent
          }
          throw error;
        }
      }}
    />
  );
}

Configure your OAuth client IDs in Google's API credentials console. The iOS client ID and Android client ID must match the bundle ID / package name of each platform build.

6. Push registration

@notifee/react-native is a renderer for incoming notifications — it does not produce APNs/FCM tokens. The tokens come from the platform-native push module: PushNotificationIOS (built into React Native) for APNs, and Firebase Messaging (@react-native-firebase/messaging) for FCM. Notifee handles permissions on both platforms.

import { Platform } from 'react-native';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import messaging from '@react-native-firebase/messaging';
import notifee, { AuthorizationStatus } from '@notifee/react-native';
import { Amba } from '@layers/amba-react-native';
 
export async function registerForPushNotifications() {
  // 1. Ask the user — Notifee unifies the prompt across iOS + Android.
  const settings = await notifee.requestPermission();
  if (settings.authorizationStatus < AuthorizationStatus.AUTHORIZED) return null;
 
  // 2. Pull the platform-native token.
  let token: string;
  if (Platform.OS === 'ios') {
    // PushNotificationIOS returns the APNs token via an event; wrap as a promise.
    token = await new Promise<string>((resolve, reject) => {
      const sub = PushNotificationIOS.addEventListener('register', (t) => {
        sub.remove();
        resolve(t);
      });
      PushNotificationIOS.addEventListener('registrationError', reject);
      PushNotificationIOS.requestPermissions();
    });
    await Amba.push.register(token, 'apns', 'com.acme.app');
  } else {
    token = await messaging().getToken();
    await Amba.push.register(token, 'fcm', 'com.acme.app');
  }
 
  return token;
}

Call this once after sign-in (or after the user opts in). Tokens rotate; re-call on app cold-start to refresh. On real iOS hardware only — APNs token issuance is disabled on the simulator.

7. Collections

Same DSL as the web SDK — identical code on bare RN and Expo:

import { Amba } from '@layers/amba-react-native';
 
await Amba.collections.insert('posts', { title: 'Hello', body: 'amba on mobile' });
const { data: posts } = await Amba.collections.find('posts', {
  order: [{ column: 'created_at', direction: 'desc' }],
  limit: 20,
});

@layers/amba-react-native carries the full where DSL (eq, ne, gt, gte, lt, lte, in, notIn, like, ilike, isNull, isNotNull, plus and, or, not) — see the Web SDK page for the full operator surface.

8. Storage uploads

Storage uploads use the standard presign → PUT → commit flow. Where the flavors diverge is in how you read a local file into bytes — bare RN typically uses react-native-blob-util (full-featured filesystem + multipart helpers); Expo uses expo-file-system.

import ReactNativeBlobUtil from 'react-native-blob-util';
import { Amba } from '@layers/amba-react-native';
 
export async function uploadAvatar(localPath: string) {
  // 1. Read file size + bytes via react-native-blob-util.
  const stat = await ReactNativeBlobUtil.fs.stat(localPath);
  const sizeBytes = Number(stat.size);
 
  // 2. Presign — server returns the upload URL + headers + asset id.
  const presign = await Amba.storage.presign({
    bucket: 'avatars',
    filename: 'avatar.jpg',
    mimeType: 'image/jpeg',
    sizeBytes,
  });
 
  // 3. PUT the file. react-native-blob-util streams the local file
  //    into the request body — no buffering the full image in JS.
  await ReactNativeBlobUtil.fetch(
    'PUT',
    presign.upload_url,
    Object.fromEntries(presign.upload_headers),
    ReactNativeBlobUtil.wrap(localPath),
  );
 
  // 4. Commit — turns the upload into a permanent MediaAsset row.
  const asset = await Amba.storage.commit(presign.upload_id, presign.asset_id);
  return asset.url;
}

Bare React Native vs. Expo workflow

ConcernBare RNExpo managed
Installpnpm add + pod-installnpx expo install
Native iOS configurationEdit Info.plist / Entitlements.plist directlyUse the @layers/amba-expo config plugin
Native Android configurationEdit AndroidManifest.xml directlyUse the @layers/amba-expo config plugin
Apple sign-in capabilityAdd via Xcode → Signing & Capabilitiesexpo-apple-authentication adds it on expo prebuild
Push entitlementsAdd via Xcode + Google Services file@layers/amba-expo plugin adds APNs entitlement + reads google-services.json
Best forExisting native iOS / Android codebases adding RNGreenfield apps, EAS Build, OTA updates

Either workflow runs the same @layers/amba-react-native runtime — the difference is only in how native config files get written.

4.0 surface highlights

The full 4.0 namespace set is the same shape as @layers/amba-web:

// users — fetch + update profiles by id.
await Amba.users.get(); // current user
await Amba.users.get('usr_abc'); // public-safe other user
await Amba.users.update({ display_name: 'Ada' }); // self
await Amba.users.update('usr_abc', { display_name: 'Ada' }); // admin (users:write)
 
// friends — unfriend by user id, no friendship-row lookup needed.
await Amba.friends.removeFriend('usr_abc'); // NEW in 4.0
 
// flags — dedicated single-key route.
const flag = await Amba.flags.get('new_onboarding'); // FlagAssignment | null
 
// sessions / sync / leagues — new namespaces.
const sessions = await Amba.sessions.list();
await Amba.sessions.revoke(sessions[0].id);
await Amba.sync.pushChanges(buffered);
const me = await Amba.leagues.me();
const cohort = await Amba.leagues.cohort();
 
// content — canonical names (no getX aliases).
const today = await Amba.content.today();
const lib = await Amba.content.library('default', { limit: 20 });
const item = await Amba.content.item('item_abc');
 
// messaging — `conversations` is canonical.
const convos = await Amba.messaging.conversations();
const message = await Amba.messaging.getMessage(conversationId, messageId);
 
// storage — list returns the cursor envelope.
const { data, next_cursor } = await Amba.storage.list('avatars/');
 
// diagnostics — wire-verify ping.
const ping = await Amba.diagnostics.ping();

Common pitfalls

  • Amba.configure() called inside a component render — calls happen on every re-render and racily replace the singleton. Always wrap in useEffect(..., []) or call at module scope above your root component.
  • Push registration on simulators — APNs requires a real device. In bare RN, PushNotificationIOS.requestPermissions() resolves without issuing a token on the simulator; in Expo, Notifications.getDevicePushTokenAsync returns null (the Expo snippet short-circuits when Device.isDevice is false). Guard accordingly.
  • Users signed out after every launch — no secure token store is installed, so the tokens live in memory. Install react-native-keychain, pass a secureStorage adapter, or opt into tokenStorage: "asyncStorage" (see Where session tokens are stored).
  • Hermes performance — on Hermes, React Native 0.73+ is required for full SDK performance. Earlier versions fall back to a slower JS-only path that's ~3× slower.

See also

  • Expo SDK — the config plugin layer on top of this package.
  • Client API reference — HTTP endpoint reference for every namespace.
  • Code samples — same operations side-by-side with the other 7 SDKs.