Amba

An Agent Builds an App

The end-to-end reference for a coding agent — account, project, data model, SDK, identity, XP + streaks, push, live updates, and the ship checklist, in order, with real calls.

This is the reference build. It walks the entire journey from "no account" to "shipping app" in nine steps, and every call on this page is real — the MCP tool names, argument shapes, and SDK methods are taken from the live surface, not paraphrased. If you are a coding agent, you can execute this page top to bottom.

The example app is a language-practice app, but every step generalizes. For a one-page map of everything else Amba can do, see the Capability Index.

0. Entry points

Two ways in, identical results:

  • MCP (you have mcp.amba.dev configured): everything below is direct tool calls. Start at step 1.
  • No MCP yet: run npx -y @layers/amba init in the project directory. It provisions the account + project headlessly, writes .env.local, and wires the MCP server into every detected client config — steps 1 and 2 collapse into that one command. See MCP install.

1. Create the account and project

Call amba_developer_signup — no Authorization header needed:

amba_developer_signup({
  "email": "dev@example.com",
  "password": "a-strong-password"
})

The response carries everything you need:

  • pat — your developer Personal Access Token. Pass it as an inline pat argument on every subsequent amba_* call this session; write the matching mcp_config snippet to the MCP client config so future sessions authenticate automatically.
  • project.project_id, project.client_key, project.server_key — a real isolated project, provisioning asynchronously.
  • project.verify_token — verify the account headlessly:
amba_developer_verify({ "verify_token": "<project.verify_token>" })

Provisioning takes ~10 seconds. Poll until status is active before sending client traffic:

amba_projects_get_provisioning_status({ "pat": "...", "project_id": "<project.project_id>" })

Write the keys where the app will read them:

# .env.local
AMBA_PROJECT_ID=<project.project_id>
AMBA_CLIENT_KEY=<project.client_key>
AMBA_SERVER_KEY=<project.server_key>
AMBA_API_URL=https://api.amba.dev

client_key ships in the app. server_key is for server-side code only — never embed it in a client bundle.

2. Model the data

Collections are schema-first typed tables with automatic per-user row scoping. Create one for practice decks:

amba_collections_create({
  "pat": "...",
  "project_id": "...",
  "name": "decks",
  "columns": [
    { "name": "title", "type": "text", "nullable": false },
    { "name": "language", "type": "text", "nullable": false },
    { "name": "cards", "type": "jsonb" },
    { "name": "difficulty", "type": "integer" }
  ]
})

The server adds id, user_id, created_at, updated_at, deleted_at and applies the change via an async workflow — the response includes the workflow status. Columns are NOT NULL unless you pass "nullable": true; column defaults are not supported, so set values like difficulty at insert time. Column types: text, integer, bigint, boolean, jsonb, timestamptz, date, uuid, numeric, vector (with dimension), plus array types (text[], integer[], …).

Seed shared catalog data with the atomic batch insert (up to 500 rows):

amba_admin_insert_rows({
  "pat": "...",
  "project_id": "...",
  "name": "decks",
  "rows": [
    { "title": "Spanish basics", "language": "es", "difficulty": 1 },
    { "title": "French verbs", "language": "fr", "difficulty": 2 }
  ]
})

Per-user data vs shared catalog. Collections default to strict per-user scoping — each signed-in user reads only their own rows. For a shared, read-by-everyone catalog, create the collection with shared: true, or use Content Libraries for scheduled editorial content. Choosing the wrong primitive here is the most common migration wrong-turn — see Collections for the chooser.

3. Install and initialize the SDK

Pick the package for the stack (the runtime surface is identical everywhere — see the parity matrix). For an Expo app:

npx expo install @layers/amba-expo @react-native-async-storage/async-storage \
  expo-notifications expo-device expo-apple-authentication \
  expo-auth-session expo-crypto expo-linking
// app/_layout.tsx
import { useEffect } from 'react';
import { Slot } from 'expo-router';
import { Amba } from '@layers/amba-expo';
 
export default function RootLayout() {
  useEffect(() => {
    Amba.configure({
      projectId: process.env.EXPO_PUBLIC_AMBA_PROJECT_ID!,
      clientKey: process.env.EXPO_PUBLIC_AMBA_CLIENT_KEY!,
    }).catch((err) => console.warn('Amba.configure failed', err));
  }, []);
 
  return <Slot />;
}

(Expo exposes only EXPO_PUBLIC_-prefixed env vars to client code — copy the values from .env.local under that prefix. Web apps use @layers/amba-web and import the same Amba object; see Quickstart for the full Expo walkthrough including the config plugin.)

4. Identity

configure() seeds a stable anonymous id; the first authenticated call signs in anonymously on its own, so the app works before any sign-up UI exists. When you add accounts:

// Email + password
const result = await Amba.auth.signUpWithEmail(email, password);
// or: await Amba.auth.signInWithEmail(email, password);
 
// Sign in with Apple — Expo helper drives the native flow end-to-end
const appleResult = await Amba.signInWithApple();
 
// On web / bare RN, pass the platform token yourself:
// await Amba.auth.signInWithApple(identityToken);
// await Amba.auth.signInWithGoogle(idToken);

Account linking preserves the anonymous user's history — XP, streaks, and collection rows follow the user through sign-up. See /auth.

5. First feature: XP + streaks

Define the rules from the admin side. Award XP whenever a lesson completes, capped at 10 awards per day:

amba_xp_rules_create({
  "pat": "...",
  "project_id": "...",
  "name": "Lesson completed",
  "event_name": "lesson_completed",
  "xp_amount": 25,
  "max_per_day": 10
})

Add a daily practice streak keyed for SDK use:

amba_streaks_create({
  "pat": "...",
  "project_id": "...",
  "key": "daily_practice",
  "name": "Daily Practice",
  "qualifying_event": "lesson_completed",
  "period": "daily"
})

The client side is two calls — track the event (XP and the streak both fire from it server-side) and read the results:

// When the user finishes a lesson:
await Amba.events.track('lesson_completed', { deck_id: deckId, score: 92 });
 
// Read back state for the UI:
const balance = await Amba.xp.getBalance(); // total XP + current level
const streaks = await Amba.streaks.getAll(); // current/longest counts
 
// Or skip polling entirely — stream the user's own gamification changes:
const unsub = Amba.gamification.subscribe((change) => {
  // change.kind: "xp" | "level" | "achievement" | "streak"
  if (change.kind === 'level') showLevelUp(change.data);
});

Before wiring UI, verify the rules do what you think with the dry-run — it returns what a track call would trigger without writing anything:

const plan = await Amba.events.explain('lesson_completed', { score: 92 });
// plan.matched → the XP rule and streak qualification that would fire

6. Push notifications

Configure credentials once. They're validated at configure time against the exact contract the send path uses, and stored encrypted:

amba_integrations_configure({
  "pat": "...",
  "project_id": "...",
  "provider": "apns",
  "config": {
    "key_id": "ABC123DEFG",
    "team_id": "TEAM123456",
    "bundle_id": "com.example.app",
    "apns_key_p8": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
    "environment": "sandbox"
  }
})

(For Android, provider: "fcm" with service_account_json. Full field reference: Push setup.)

Register the device from the app — the Expo helper captures the token and registers it in one call, picking APNs on iOS and FCM elsewhere:

const tokenRecord = await Amba.registerPushToken();

Prove the loop end-to-end before building campaigns:

amba_push_send_test({
  "pat": "...",
  "project_id": "...",
  "title": "It works",
  "body": "Push is wired.",
  "app_user_id": "<the test user's id>"
})

Then target real cohorts — create a segment and a campaign against it:

amba_segments_create({
  "pat": "...",
  "project_id": "...",
  "name": "Lapsed 7d",
  "rules": {
    "operator": "AND",
    "conditions": [{ "field": "last_seen_at", "op": "not_within", "value": "7d" }]
  }
})
 
amba_push_campaigns_create({
  "pat": "...",
  "project_id": "...",
  "title": "Your streak misses you",
  "body": "One lesson keeps it alive.",
  "segment_id": "<segment id from the previous call>",
  "scheduled_at": "2026-07-01T16:00:00Z"
})

Segments re-evaluate every 15 minutes; campaigns deliver on schedule. See Push notifications.

7. Live updates

Subscribe to collection changes so the UI updates the moment a row lands — no polling. Requires an authenticated session (step 4):

const unsubscribe = Amba.collection('decks')
  .where('language', 'es')
  .subscribe((change) => {
    // change.op: "INSERT" | "UPDATE" | "DELETE"; change.row: the row
    applyChange(change);
  });
 
// later: unsubscribe();

Live updates are generally available, including for shipped apps: long-lived subscriptions connect to a dedicated realtime host (realtime.amba.host) with no request-duration ceiling, and the SDK routes there automatically — in @layers/amba-web ≥ 4.0.6, @layers/amba-node ≥ 4.0.7, and @layers/amba-react-native ≥ 4.0.5 (Expo inherits via React Native). If the app pins outbound hosts, allowlist realtime.amba.host alongside api.amba.dev. Conversations (Amba.messaging.conversation(id).subscribe) and gamification (Amba.gamification.subscribe) stream over the same channel. See Live Updates.

8. Ship

The pre-release checklist, each item verifiable by a call:

  1. Account verifiedamba_developer_verify returned success (step 1), or amba_developer_me shows is_verified: true.

  2. Provisioning activeamba_projects_get_provisioning_status reports active.

  3. Push proven end-to-endamba_push_send_test landed on a real device with production-environment credentials (re-run amba_integrations_configure with "environment": "production" for the App Store build).

  4. Keys in the right placesclient_key in the app bundle, server_key only in server-side code and CI secrets.

  5. Spend guarded — set a project spend ceiling so a runaway client can't surprise anyone:

    amba_billing_set_ceiling({ "pat": "...", "project_id": "...", "ceiling_usd": 50 })

    and read headroom anytime with amba_billing_status. See /billing.

  6. Promote configuration, not clicks — when you split dev/prod into separate projects, export the dev project's configuration as a declarative bundle and import it into prod in one call. See Environment promotion.

From here, the rest of the surface is one tool call away — entitlements and paywalls, virtual currencies, social, AI prompts, serverless functions, and more. The Capability Index is the map.

On this page