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
  // sessionPersistence: 'local',          // or 'memory' (see below)
});
 
export { Amba };

Session persistence

sessionPersistence decides where the signed-in session lives between page loads:

  • 'local' (default) keeps the session token, refresh token, user id and anonymous id in localStorage, so reloads and new tabs stay signed in. Any script running on your origin can read localStorage, so a cross-site scripting bug can copy the refresh token (valid for 90 days) and keep using the session after the tab closes.
  • 'memory' writes none of that identity to storage. It lives in the page and ends with it, so a script injected later finds no stored token to copy. Every reload or new tab starts signed out with a new anonymous identity. Configuring with 'memory' also removes an identity an earlier 'local' configuration saved.

Pick 'memory' for apps that handle sensitive data or render untrusted content and can ask users to sign in each visit. Queued offline writes stay in localStorage in both modes.

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
// (in localStorage; see "Session persistence" for the memory-only mode).

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.dev → Events (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); // { key: value } map of each key's default value
 
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 — today() returns the named schedule's item for today.
const today = await Amba.content.today();
const lib = await Amba.content.library('Daily Tips', { limit: 20 });
const item = await Amba.content.item(lib[0]!.id);
 
// 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