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({ apiKey }) 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({
  apiKey: import.meta.env.VITE_AMBA_API_KEY!,
  // Optional:
  // baseUrl: '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_API_KEY=amb_dev_ck_XXXX in your .env.local (replace amb_dev_ck_XXXX with the key from amba init or app.amba.dev).

For Next.js use NEXT_PUBLIC_AMBA_API_KEY; for Create React App use REACT_APP_AMBA_API_KEY. Anything client-readable works — the dev key is safe to ship (server-side auto-scoped scopes 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.signInWithSocial('apple', appleIdentityToken);
 
// Google (pass the id_token from Google Identity Services)
await Amba.auth.signInWithSocial('google', 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 amba storage create-bucket avatars --public. 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;

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 — the Amba web SDK's runtime initializer requires 'wasm-unsafe-eval' in your script-src CSP directive. Without it, the browser blocks the SDK from initializing.

See also

On this page