Amba

Node SDK

Server-side quickstart for @layers/amba-node — install, configure, server keys, asUser() per-request scoping, and Connect/Express middleware in under 10 minutes.

@layers/amba-node is the Node.js server SDK. Same surface as @layers/amba-web, idiomatic for long-running Node processes — one client per service, per-request user scoping via asUser(uid) with a server key, and a built-in Connect-style middleware factory.

When to use this SDK:

  • Webhook handlers (Stripe, RevenueCat, GitHub) that need to mutate amba state from server context.
  • Background workers / cron jobs that call amba on behalf of users without a session token.
  • BFFs that proxy specific operations from a browser to amba with extra business logic.

1. Install

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

Requires Node 20+ (uses native fetch, top-level await, and import.meta.resolve).

ESM only

@layers/amba-node ships as ESM-only ("type": "module" in package.json). It cannot be require()'d from a CommonJS file.

If your project is still CJS, either flip the consuming file to ESM:

// package.json of your service
{ "type": "module" }

…or load the SDK via dynamic import() from CJS:

// CommonJS context
const { AmbaClient } = await import('@layers/amba-node');
const amba = await AmbaClient.configure({ projectId, clientKey, serverKey });

tsx, ts-node with --esm, Next.js server routes, NestJS (with "type": "module"), Hono, and Cloudflare Workers all default to ESM and require no change.

2. Configure once at boot

AmbaClient.configure() is a one-time async call that returns the configured client. Hold the instance for the life of the process.

// src/amba.ts
import { AmbaClient } from '@layers/amba-node';
 
export const amba = await AmbaClient.configure({
  projectId: process.env.AMBA_PROJECT_ID!,
  clientKey: process.env.AMBA_CLIENT_KEY!,
  serverKey: process.env.AMBA_SERVER_KEY!, // required for asUser()
  // apiUrl: 'https://api.amba.dev', // default
});

Set all three environment variables. AMBA_PROJECT_ID and AMBA_CLIENT_KEY come from your project's dashboard or CLI output. AMBA_SERVER_KEY is a separate credential — see Server keys below.

3. Server keys

A server key (amb_dev_sk_… / amb_live_sk_…) is a project-scoped backend credential analogous to a Firebase service account or Supabase service-role key. It grants access to project-level admin operations without requiring a developer login — making it safe to use in long-running services and CI pipelines.

Generate a server key:

amba api-keys create --type server --env production

Or via MCP:

amba_api_keys_create { key_type: "server", environment: "production" }

Comparison to other credential types:

CredentialScopeUse case
Developer PAT (amb_dpat_…)Your accountOne-time setup, project creation, CLI
Client key (amb_dev_ck_… / amb_live_ck_…)Project, client-sideBrowser SDK, mobile SDK
Server key (amb_dev_sk_… / amb_live_sk_…)Project, server-sideNode SDK, asUser(), backend automation

Server keys can authenticate all project-scoped admin operations but cannot create new projects or list projects across accounts — those require a developer PAT.

4. Per-request user scoping with asUser()

asUser(uid) returns a scoped handle that attributes every API call to the given app_user.id. It uses the server key + an internal on-behalf-of mechanism — no session token is minted or managed per user. This makes it efficient for bulk operations:

// src/routes/orders.ts
import { amba } from '../amba.js';
 
router.post('/orders', async (req, res) => {
  const userId = req.session.userId;
  const scoped = amba.asUser(userId);
 
  const order = await scoped.collections.insert('orders', {
    items: req.body.items,
    total: req.body.total,
  });
 
  await scoped.events.track('order_created', { order_id: order.data.id });
 
  res.json({ order: order.data });
});

Bulk import — no round-trip auth per user:

for (const user of importedUsers) {
  await amba.asUser(user.id).events.track('account_migrated', {
    source: 'legacy_system',
  });
}

asUser(uid) is O(1) — each call just captures the userId. The network request happens only when you call a method on the returned handle.

asUser(uid) returns a handle that:

  1. Wraps every read with the equivalent of WHERE user_id = uid.
  2. On insert, sets the user_id column to uid.
  3. On delete, restricts to rows where user_id = uid.

Amba validates that uid exists in your project before acting on any request — a userId from a different project is rejected.

5. First track (post-auth)

// src/routes/webhook-stripe.ts
import { amba } from '../amba.js';
 
export async function handleStripeWebhook(event: Stripe.Event) {
  if (event.type === 'checkout.session.completed') {
    const userId = await resolveAmbaUserFromStripeSession(event.data.object);
 
    await amba.asUser(userId).events.track('checkout_completed', {
      session_id: event.id,
      amount: event.data.object.amount_total,
    });
  }
}

Without asUser(), the Node SDK has no user context and user-scoped client endpoints require one. Use asUser() with a server key for every request that needs to act on behalf of a specific user.

5. Connect/Express middleware

For request-handlers that always need the SDK, amba.middleware() returns a Connect-style factory that attaches req.amba to every incoming request:

import express from 'express';
import { amba } from './amba.js';
 
const app = express();
app.use(express.json());
app.use(amba.middleware());
 
app.post('/webhook', (req, res) => {
  req.amba.events.track('webhook_received', { source: 'stripe' });
  res.sendStatus(204);
});
 
app.listen(3000);

For a per-request user-scoped handle, chain req.amba.asUser(req.session.userId) inside each handler.

Collections

Same DSL as the web SDK:

const { where } = amba.collections;
 
const { data: orders } = await amba.collections.find('orders', {
  filter: where.eq('status', 'paid'),
  limit: 100,
});
 
const order = await amba.collections.findOne('orders', 'ord_123');
 
await amba.collections.update('orders', 'ord_123', { status: 'refunded' });
await amba.collections.delete('orders', 'ord_123'); // soft delete

For full operator coverage (eq, ne, gt, gte, lt, lte, in, notIn, like, ilike, isNull, isNotNull, and, or, not) see collections.

Storage (presign + commit)

The Node SDK exposes presign and commit directly so the upload itself can flow through your server — useful for buffered uploads, validation, or virus scanning:

const presign = await amba.storage.presign({
  bucket: 'invoices',
  filename: `invoice-${order.id}.pdf`,
  mimeType: 'application/pdf',
  sizeBytes: pdfBuffer.byteLength,
  retentionDays: 365,
});
 
await fetch(presign.upload_url, {
  method: 'PUT',
  headers: Object.fromEntries(presign.upload_headers),
  body: pdfBuffer,
});
 
const asset = await amba.storage.commit(presign.upload_id, presign.asset_id);
console.log(asset.url);

Push fan-out

If your service is the source of truth for "send a push to user X", call register() once when the device hands you a token, and use the push campaign API (admin) to fan out from your server:

await amba.asUser(userId).push.register(deviceToken, 'apns', 'com.acme.app');

Counting events

For dashboards, billing aggregations, or any "how many <event> events fired this week" question, hit the admin events/count endpoint with your server key. The Node SDK doesn't wrap this surface (track is the only SDK-side event method; reads are an admin concern).

const res = await fetch(
  `${process.env.AMBA_API_URL ?? 'https://api.amba.dev'}/v1/admin/projects/${projectId}/events/count?` +
    new URLSearchParams({
      name: 'order_completed',
      since: '2026-05-01T00:00:00Z',
      until: '2026-05-15T00:00:00Z',
      bucket: 'day', // 'hour' | 'day' | 'week' | 'month' — omit for a single total
    }),
  { headers: { Authorization: `Bearer ${process.env.AMBA_SERVER_KEY}` } },
);
const counts = await res.json();
// counts.data: [{ bucket: '2026-05-01', count: 42 }, ...] when `bucket` is set;
// otherwise counts.data is a scalar `{ count: number }`.

See admin/events for the full filter shape (per-user, per-project segment, properties match).

SDK version

@layers/amba-node exports a runtime constant for telemetry / debug logging:

import { SDK_VERSION } from '@layers/amba-node';
console.log(`amba-node ${SDK_VERSION}`);

SDK_VERSION is also stamped onto every outbound request as X-SDK-Version: amba-node/<version> so the server can correlate behaviour to a specific SDK build.

AI proxy

const reply = await amba.ai.anthropic.messages.create({
  prompt_slug: 'order_summary',
  variables: { order_id: order.id, items: order.items },
  max_tokens: 512,
});

Same prompt slug catalog as the client SDK — slugs are managed in the console and shared across server + browser usage.

Long-running processes

The Node SDK is safe for long-lived processes:

  • The underlying amba core caches HTTP connections.
  • Auth token refresh is automatic.
  • There's no global state — multiple AmbaClient instances (e.g. one per tenant key in a multi-tenant proxy) are isolated.

For high-concurrency scenarios (10k+ rps), pool one AmbaClient per worker process rather than per request.

Common pitfalls

  • Calling AmbaClient.configure() per request — don't. The cold-start init is ~50ms; call once at boot, share the instance.
  • Forgetting asUser(uid) in user-context handlers — the default surface is server-trusted; without asUser you can accidentally read or mutate another user's data. Lint rule: every route handler that takes user input should explicitly choose amba.asUser(uid) or amba (admin).
  • WebSocket / SSE proxies — auth state isn't shared with the browser; mint a short-lived session in the browser instead of proxying through Node.

See also

On this page