Amba

Web SDK

Browser quickstart for @layers/amba-web — install, configure, first track, first auth, first collection insert in under 10 minutes.

@layers/amba-web is the browser SDK. It works in any modern browser; no framework required.

Bundle footprint: ~180 KB gzipped total, loaded lazily on first Amba.configure().

1. Install

pnpm add @layers/amba-web
# or: npm i @layers/amba-web
# or: yarn add @layers/amba-web

Works in any modern bundler — Vite, Next.js, SvelteKit, Astro, Remix, Webpack, Rspack. Pure ES module, sideEffects: false, tree-shakes cleanly.

2. Configure at app start

Call Amba.configure({ projectId, clientKey }) once before any other SDK method. The first call initializes the SDK and seeds an anonymous identity from localStorage.

// src/lib/amba.ts
import { Amba } from '@layers/amba-web';
 
await Amba.configure({
  projectId: import.meta.env.VITE_AMBA_PROJECT_ID!,
  clientKey: import.meta.env.VITE_AMBA_CLIENT_KEY!,
  // Optional:
  // apiUrl: 'https://api.amba.dev',       // default
  // consentRequired: false,               // gate events behind explicit consent
  // debug: import.meta.env.DEV,           // verbose console logs
});
 
export { Amba };

Set VITE_AMBA_PROJECT_ID and VITE_AMBA_CLIENT_KEY in your .env.local (values from amba init or app.amba.dev).

For Next.js use NEXT_PUBLIC_AMBA_PROJECT_ID / NEXT_PUBLIC_AMBA_CLIENT_KEY; for Create React App use REACT_APP_AMBA_PROJECT_ID / REACT_APP_AMBA_CLIENT_KEY. Anything client-readable works — the client key is safe to ship (server-side scoping limits every request to the signed-in user; the key itself can't read another tenant's data).

3. First auth

Sign in anonymously to get an appUserId you can attach data to. Every other client-side call (events.track, collections.insert, storage.upload, …) is gated on a live session, so this step comes before any of those. For email / Apple / Google sign-in see the auth section.

import { Amba } from '@/lib/amba';
 
const session = await Amba.auth.signInAnonymously();
console.log('signed in as', session.user.id);
 
// `Amba.appUserId` is now populated and persists across page reloads
// (LocalStorage-backed via amba's persistence module).

4. First event

Now that the SDK holds a session token, track an event:

import { Amba } from '@/lib/amba';
 
await Amba.events.track('app_opened', { source: 'direct' });

You can confirm the event landed by visiting the project dashboard at app.amba.devEvents (typically visible within ~10 seconds).

Why auth first? Every /v1/client/* endpoint except the auth flows themselves runs under clientSessionAuth server-side. Calling events.track (or any other namespace method) before signInAnonymously returns 401 Unauthorized — session token missing or expired. Anonymous-but-unauthenticated event tracking — attributing events to an anonymous_id with no session token — isn't supported in v1.

5. First collection insert

Insert a row into a collection you've already created via amba collections create posts --field title:text --field body:text. The server stamps user_id from your session token, so client-supplied user_id is ignored.

import { Amba } from '@/lib/amba';
 
const post = await Amba.collections.insert<{ id: string; title: string; body: string }>('posts', {
  title: 'Hello amba',
  body: 'My first post.',
});
 
const { data: myPosts } = await Amba.collections.find<typeof post>('posts', {
  order: [{ column: 'created_at', direction: 'desc' }],
  limit: 20,
});

Auto-RLS is enforced server-side: even if you call find with no filter, you only see your own rows.

Authentication

// Anonymous
await Amba.auth.signInAnonymously();
 
// Email
await Amba.auth.signUpWithEmail('me@example.com', 'correct horse battery staple');
await Amba.auth.signInWithEmail('me@example.com', 'correct horse battery staple');
 
// Apple (pass the identityToken from `AppleID.auth.signIn`)
await Amba.auth.signInWithApple(appleIdentityToken);
 
// Google (pass the id_token from Google Identity Services)
await Amba.auth.signInWithGoogle(googleIdToken);
 
// Read current session
const me = await Amba.auth.me();
 
// Sign out
await Amba.auth.signOut(/* rotateAnonymousId? */ false);

Refresh tokens are rotated server-side on every refresh and stored as sha256 hashes. The SDK schedules silent token refresh ahead of expiry; you generally never have to call refresh() manually.

Collections

const { where } = Amba.collections;
 
// Equality
await Amba.collections.find('posts', {
  filter: where.eq('status', 'published'),
});
 
// Boolean composition
await Amba.collections.find('posts', {
  filter: where.and(
    where.eq('status', 'published'),
    where.gte('score', 100),
    where.or(where.like('title', 'Hello%'), where.in('tag', ['featured', 'trending'])),
  ),
  order: [{ column: 'created_at', direction: 'desc' }],
  limit: 50,
});
 
// Cursor pagination
const page1 = await Amba.collections.find('posts', {
  order: [{ column: 'created_at', direction: 'desc' }],
  limit: 50,
});
const page2 = await Amba.collections.find('posts', {
  order: [{ column: 'created_at', direction: 'desc' }],
  limit: 50,
  cursor: page1.next_cursor!,
});
 
// Find one / update / delete
const post = await Amba.collections.findOne('posts', 'post_123');
await Amba.collections.update('posts', 'post_123', { title: 'New title' });
await Amba.collections.delete('posts', 'post_123'); // soft delete

Supported operators on where: eq, ne, gt, gte, lt, lte, in, notIn, like, ilike, isNull, isNotNull, plus and, or, not for composition. See collections for the full DSL.

Storage uploads

storage.upload() runs the full presign → PUT → commit flow against your storage bucket.

async function onFileChange(e: Event) {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (!file) return;
 
  const asset = await Amba.storage.upload({
    bucket: 'avatars',
    file,
    retentionDays: 30, // optional
  });
 
  console.log(asset.url); // public CDN URL
}

Buckets are created via the admin API — POST /admin/projects/:projectId/storage/buckets with { "name": "avatars", "public": true }. See storage for retention policies and image transforms.

Push (web push)

const registration = await navigator.serviceWorker.register('/amba-sw.js');
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY',
});
 
await Amba.push.register(JSON.stringify(subscription), 'web');
await Amba.push.subscribe('breaking_news');

AI proxy

const reply = await Amba.ai.anthropic.messages.create({
  prompt_slug: 'support_assistant',
  variables: { user_query: 'How do I cancel?' },
  max_tokens: 1024,
});
 
console.log(reply.content);

Provider keys live server-side. You only ship the prompt slug (managed in the console) and runtime variables. See AI gateway.

Config + flags

const config = await Amba.config.fetch();
console.log(config.values); // resolved per-user config map
 
const flags = await Amba.flags.fetch();
const newUi = flags.find((f) => f.name === 'new_ui')?.enabled ?? false;

4.0 surface highlights

Namespaces shipped (or expanded) in 4.0:

// users — fetch and update profiles. `userId` omitted = current user.
await Amba.users.get(); // GET /v1/client/users/me
await Amba.users.get('usr_abc'); // public-safe profile of another user
await Amba.users.update({ display_name: 'Ada' }); // self
await Amba.users.update('usr_abc', { display_name: 'Ada' }); // admin (requires users:write)
 
// friends — request/accept/block + unfriend by user id.
await Amba.friends.removeFriend('usr_abc'); // NEW in 4.0
 
// flags — single-key lookup alongside the bulk fetch.
const flag = await Amba.flags.get('new_onboarding'); // FlagAssignment | null
const all = await Amba.flags.fetch();
 
// sessions — list and revoke server-tracked app sessions.
const sessions = await Amba.sessions.list();
await Amba.sessions.revoke(sessions[0].id);
 
// sync — offline change replay.
const result = await Amba.sync.pushChanges(buffered);
const pulled = await Amba.sync.pullChanges({ entity_type: 'todos' });
 
// leagues — tiered leaderboard cohorts.
const me = await Amba.leagues.me();
const cohort = await Amba.leagues.cohort();
 
// content — canonical 4.0 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 the canonical name.
const convos = await Amba.messaging.conversations();
const message = await Amba.messaging.getMessage(conversationId, messageId);
 
// storage — list returns the cursor envelope { data, next_cursor }.
const { data, next_cursor } = await Amba.storage.list('avatars/');
 
// diagnostics — wire-verify primitive.
const ping = await Amba.diagnostics.ping();

Common pitfalls

  • Calling Amba.* before await Amba.configure(...) resolves throws "amba SDK not configured". Either await it at app entry or guard with Amba.appUserId === undefined.
  • SDK loaded twice — the entry-point loader is idempotent. If you see duplicate Amba module requests in the network tab, your bundler is duplicating the module — check for two copies of @layers/amba-web in pnpm why @layers/amba-web.
  • Strict Content Security Policy — add 'wasm-unsafe-eval' to your script-src CSP directive so the Amba web SDK can initialize. Without it, a strict CSP blocks the SDK at startup.

See also

On this page