Kitchen-sink Expo tutorial
Step-by-step build of a to-do list Expo app that exercises the whole Amba SDK surface — auth, collections, streaks, remote config, and push.
In this tutorial you'll build a real Expo to-do app that exercises every major Amba feature:
- Email signup and login.
- Todos stored as rows in an owner-scoped collection, private to each user (read and written from the app).
- A daily "completed a todo" streak.
- A remote-config feature flag (
weekly_goal_count). - Push notifications on iOS and Android.
When you're done you'll have a TestFlight- and Play-Internal-ready app that uses every major Amba feature.
1. Prerequisites
- Node.js 20 or later (
node -v). Node 22 also works. - Expo CLI — invoked via
npx, no global install needed. - iOS only: Xcode 15+ for local dev builds, or an Expo account for EAS Build.
- Android only: Android Studio with an emulator, or a physical device with USB debugging.
You don't need an Apple Developer account to build and run the app, but you will need one ($99/yr) to test iOS push notifications and submit to TestFlight. Skip the iOS push section if you don't have one.
On iOS, push notifications require a development build — Expo Go can't deliver them. Plan to test push on the development build, not in Expo Go.
2. Install the SDK
Start from a fresh Expo project:
npx @layers/amba init walks you through:
- Sign in — opens a browser for login (or reuses cached credentials).
- Pick or create a project — choose an existing project or name a new one.
- Mint a client key — generates a development client key (prefixed
amba_ck_). - Write
.env.local— yourprojectId,clientKey, andapiUrlland in the project root. - Drop agent context — writes
AMBA.mdand.cursor/rules/amba.mdcso Cursor / Claude Code know how to drive Amba in your project.
No Amba account yet? Run npx @layers/amba init; signup is open, so no invite code is needed. See Getting access.
Then install the Expo SDK and its peer dependencies:
3. Environment variables
Expo only exposes env vars that start with EXPO_PUBLIC_ to client-side code. Rewrite .env.local so the Amba values use the Expo prefix:
There are no server-side env vars required for the mobile app itself. If you later add an edge function or a server that calls the Admin API, it will need un-prefixed developer credentials (AMBA_DEVELOPER_TOKEN=...) — never ship those to the client.
.gitignore in the Expo template already excludes .env*.local. Confirm before committing.
4. Configure the Expo plugin
The @layers/amba-expo config plugin auto-wires push entitlements, Apple Sign In capability, and URL schemes so you don't have to hand-edit Info.plist / AndroidManifest.xml / app.json. Register the plugin and pass any options you want:
What the plugin adds for you:
- iOS —
aps-environmententitlement (defaultdevelopment, EAS flips toproductionat build time),applinks:associated domains, andCFBundleURLTypesfor deep-link URL schemes. Apple Sign In capability is enabled via the peerexpo-apple-authenticationplugin. - Android — intent filters on
.MainActivityfor each scheme/host/pathPrefix you pass.autoVerifyis added automatically when the scheme ishttps.
You never need to hand-edit Info.plist or AndroidManifest.xml for basic push + deep-link wiring. Rebuild native after changing app.json:
Pick your own bundleIdentifier / package — you'll need them later for APNs / FCM.
5. Initialize the SDK
Edit (or create) app/_layout.tsx:
If you're starting from the create-expo-app --template default template, keep the existing ThemeProvider + <Stack> wrapper around this — merge the ready-check into the template's root layout rather than replacing it wholesale.
Amba.configure() does three things for you:
- Initializes the on-device SDK singleton with your project + client key.
- Restores any persisted session and anonymous id from device storage.
- Boots the in-memory caches (events queue, config, flags).
On iOS and Android builds, the session and refresh tokens are stored in SecureStore (Keychain on iOS, EncryptedSharedPreferences on Android) when expo-secure-store and AsyncStorage are both installed and SecureStore is available on the device. Otherwise the SDK logs a warning and keeps the tokens in memory, so users sign in again after each launch (tokenStorage: "asyncStorage" in Amba.configure() opts into plain AsyncStorage instead). Keep expo-secure-store in the install command above for any app that signs users in.
Push registration is opt-in — call Amba.registerPushToken() explicitly when you're ready to ask for the permission prompt (see step 7).
6. Auth: email signup + login
Create app/(auth)/signin.tsx:
Now gate the home tab on having a session. With the default template, (tabs)/index.tsx is the home route; drop the auth gate in there (don't create a new app/index.tsx, or it will race with (tabs)/index.tsx):
Amba.auth.signOut() fires onAuthStateChange(null), so the subscription redirects the user back to the sign-in screen automatically.
Full auth surface (every method available on Amba.auth):
For the Expo one-liner social flows use Amba.signInWithApple() and Amba.signInWithGoogle() — they wrap the native prompt and then call Amba.auth.signInWithApple(identityToken) / Amba.auth.signInWithGoogle(idToken) for you. See Authentication for details.
Run npx expo start and verify you can sign up, sign in, and sign out.
7. Todos: store them in an owner-scoped collection
Store personal todos in an owner-scoped collection. The app creates each todo through the authenticated client Collections API, so the server assigns the row's owner from the signed-in session. A collection uses the owner policy by default: each user reads only the rows they own (plus unowned rows, which only a shared collection can hold), and only the owner can update or delete a row. The app calls these APIs directly, with no server of your own in front.
Content libraries hold shared content that every user of the project can read, so they must not hold private todos.
Create the collection (admin-side, one time)
The collection's schema is admin-scoped: you define it once, and every user's todos land in it as separate rows. With the CLI:
Or with an MCP-enabled agent like Cursor or Claude Code with the Amba MCP server configured (see MCP):
Or via the Admin API directly:
owner is the default for both policies; the examples spell it out so the privacy rule is visible. The server adds id, user_id, created_at, updated_at, and deleted_at to every row. Leave shared off: a shared collection accepts rows without an owner, and every user can read those rows.
Read, write, update, and delete todos in the app
The client SDK's collections module exposes:
Every call needs a signed-in session, which the auth gate from step 6 provides. On insert the server sets user_id from the session and ignores any user_id in the payload. update and delete match only rows the signed-in user owns, and return 404 for a row that belongs to someone else. delete is a soft delete: the row stops appearing in reads.
Create app/todos.tsx (or add it as a new tab in (tabs)/ if you want it inside the tab bar — with the default template it's easier to add to the existing tabs group):
To make the screen reachable from the tab bar, add a Link to (tabs)/index.tsx or create a new tab file in app/(tabs)/ that re-exports this screen.
8. Streaks: track "completed a todo today"
Define the streak (admin-side)
Via MCP:
Capture the streak_definition_id — you'll only need it if you plan to call qualify() manually. If you're letting track('todo_completed') drive qualification (as the todo screen above does), no client-side id is required.
Fire the event when a todo is completed
The track() call in Step 7's toggle handler is all you need — the server matches the event name against every streak definition and qualifies whichever ones match:
You can also force-qualify without firing an event:
Read the current streak
Import it into (tabs)/index.tsx (or todos.tsx) wherever you want it rendered.
9. Remote config: weekly_goal_count
Set the flag via the CLI:
Or via MCP:
Read it in the app. The Expo wrapper exposes the config module as Amba.config:
config.fetch() returns the full ConfigBundle ({ version, values }); read individual keys off values.
Per-segment override
Set the value to 10 for users in a power_users segment:
10. Push notifications
Expo plugin handles the native wiring
You already registered @layers/amba-expo as an Expo config plugin in Step 4. That means aps-environment, Apple Sign In capability, URL schemes, and Android intent filters are all added automatically when you prebuild or run expo run:ios / eas build.
You do not need to hand-edit:
ios/*.entitlementsios/Info.plist(deep-link schemes)android/app/src/main/AndroidManifest.xml(intent filters)
Push on iOS still requires a development build — Expo Go cannot add custom entitlements. npx expo run:ios or eas build --profile development both produce a dev build.
Configure APNs (iOS)
Via MCP:
If you don't have an Apple Developer account, skip this and rely on Android for push testing.
Configure FCM (Android)
Verify the app registered its token
After a successful sign-in on a real device (push requires a physical device — simulators don't get tokens), confirm via MCP or the Admin API — the user listing includes a push_token_count per user:
If you want a custom permission flow (e.g. show your own soft-ask screen first, then prompt the OS), call Amba.registerPushToken() yourself at the moment you want the OS dialog to fire. The call requests permission, fetches the native device token, and forwards to Amba.push.register in one shot:
Send a test push
Via MCP:
Or the CLI:
amba push test sends one notification to that user's newest active push token.
Send a scheduled campaign
The API rejects scheduled_at values in the past — use a future timestamp.
11. Test on iOS and Android
Expo Go
- Works for: auth, collections, streaks, remote config.
- Does not support iOS push (custom entitlements unavailable).
- Supports Android push only with the Expo push service — for Amba-owned FCM delivery you need a development build.
Development build
A development build lets you test push on both platforms and matches your production native config.
Export without running
If you just want to confirm the app bundles, run:
Both should produce a clean bundle with no Metro errors.
12. Deploy to TestFlight / Play internal
Use EAS Build (the standard Expo CI):
Then submit:
Full EAS docs at https://docs.expo.dev/build/setup/.
What you just built
- Expo app with file-based routing.
@layers/amba-expoinitialized in the root layout — session, anonymous id, and remote config restored on launch.- Email signup + login backed by bcrypt on your project's dedicated database.
- Todos stored in an owner-scoped collection: each user creates, reads, updates, and deletes only their own rows.
- Daily streak driven by
Amba.events.track('todo_completed', ...). - Remote-config flag (
weekly_goal_count) read in the UI. - Device push tokens registered automatically on both platforms.
- Test push + scheduled campaign delivered via Amba.
Every module you just used is covered in depth in the dedicated docs: