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.

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({
      apiKey: Config.AMBA_API_KEY!, // from .env via react-native-config
      // Optional:
      // baseUrl: 'https://api.amba.dev', // default
      // consentRequired: false,
      // debug: __DEV__,
    });
  }, []);
 
  return <RootNavigator />;
}

Put AMBA_API_KEY=amb_dev_ck_XXXX in a .env file at the project root. react-native-config injects it at build time into Config.AMBA_API_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