Amba

React Native SDK

React Native quickstart for @layers/amba-react-native — install, configure, on-device session persistence, 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; sessions and anonymous identity are persisted on-device so users stay signed in across app restarts.

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-config
npx pod-install   # iOS only — links the on-device storage native module

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/Google) or a push token (APNs/FCM), pass it to Amba.auth.signInWithSocial(...) / Amba.push.register(...) unchanged. Only the producer library differs.

Crypto setup

Required. Hermes (React Native's default JS engine) does not ship crypto.getRandomValues. Amba uses it to generate session tokens and identifiers that need to be unpredictable. Without the polyfill, Amba falls back to Math.random — fine for development, not safe for production auth flows.

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. The default on-device storage adapter is installed automatically; if you need a different store (MMKV, Keychain) construct an AmbaClient directly and pass your own storage.

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 cold start, restored from
//    on-device storage on every subsequent launch.
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 default on-device storage adapter persists the session across app restarts; your users stay signed in across reboots.

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.signInWithSocial('apple', 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.signInWithSocial('google', 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.

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.
  • Sessions lost on app upgrade — the default on-device store persists across upgrades but a full app reinstall clears it. If you need cross-install persistence (rare), use iCloud Keychain on iOS + Backup Manager on Android via a custom AmbaStorage adapter.
  • 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.

On this page