Amba

Runtime

The exact runtime contract for Amba functions — module shape, the env bindings the platform injects, signed inbound headers, and error codes for calling back into the admin API.

Amba functions are JavaScript ES modules running on the Amba edge runtime. There is no wrapper, no defineFunction, no ctx.collections / ctx.auth / ctx.events object. The function source must export a default object with a fetch handler matching the Amba runtime module signature.

Handler signature

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    return new Response('hello', { headers: { 'content-type': 'application/json' } });
  },
};

Scheduled handlers and queue consumers work identically — same default-export object shape, different handler name (scheduled / queue).

env bindings (injected at deploy)

The platform injects these as env.*:

BindingTypeAlways setWhat it is
AMBA_API_URLstringyesAPI root (e.g. https://api.amba.dev).
AMBA_PROJECT_IDstringyesThis project's id
AMBA_INTERNAL_TOKENstring (secret)yesProject-scoped Bearer for /v1/admin/projects/${env.AMBA_PROJECT_ID}/* admin calls. Cannot be used for global admin (project list/create, developer auth) or for another project's URL — those reject with 401 INVALID_INTERNAL_TOKEN / 403 WRONG_PROJECT.
EDGE_HEADER_SIGNING_SECRETstring (secret)yesHMAC secret for verifying signed inbound edge headers (X-Amba-User-Id, etc.)
AMBA_AI_GATEWAY_URLstringyesAI proxy URL
STORAGEobject-storageonly when storage provisionedPer-project object-storage handle (.get/.put/.delete/.list)
<SECRET_NAME>stringwhen setCustom secrets set via amba secrets set <NAME> <value> (this is the path for any credential YOUR function needs — third-party API keys, your own developer PAT, etc.). Project-wide by default; add --function <fn> to scope to one function. See Secrets.

env does NOT carry any npm package. If your function imports from @layers/*, the bundle must inline it (the CLI bundler does this by default since 4.0.2). Imports the runtime can't resolve are rejected at upload time — see "Common upload-time errors" below.

Calling back to Amba

For per-project admin operations, pass env.AMBA_INTERNAL_TOKEN as the Bearer:

const res = await fetch(
  `${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/collections/configs/rows?key=launch_config`,
  { headers: { Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}` } },
);

The token is scope-limited to this project's /v1/admin/projects/${env.AMBA_PROJECT_ID}/* surface. The same token used at a URL referring to a different project returns 403 WRONG_PROJECT. A revoked token returns 401 INVALID_INTERNAL_TOKEN.

What this token cannot do:

  • Global admin (POST /v1/admin/projects to create a new project, GET /v1/admin/projects to list developer-owned projects, developer-auth endpoints like /auth/developer/*). Those require a developer PAT.
  • Cross-project operations. The token is bound to the project that minted it.

If you need either of those from a function, inject your own developer PAT via amba secrets set and read it via a custom binding:

amba secrets set AMBA_DEV_PAT amb_dpat_… --function <fn>

Client-plane endpoints (/client/*) work without any Bearer — they take an X-Api-Key (the project's clientKey, injectable via amba secrets set) and a user session token. If your function only needs to act on behalf of an end user, prefer those over admin.

Example: grant/spend currency from a function

Every admin surface is plain REST — call it with fetch and the project Bearer, exactly like the config example above. Currencies are a common one: grant, spend, and read transactions all live under the project admin path, so you do not need to reach for MCP from inside a function.

// Grant 100 coins to a user after they finish onboarding.
await fetch(`${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/currencies/grant`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ app_user_id: userId, currency_code: 'coins', amount: 100 }),
});
 
// Spend (debit) — returns 400 INSUFFICIENT_FUNDS if the balance is too low.
await fetch(`${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/currencies/spend`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ app_user_id: userId, currency_code: 'coins', amount: 25 }),
});
 
// Read a user's ledger (the user id is a path segment).
const txns = await fetch(
  `${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/currencies/transactions/${userId}`,
  { headers: { Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}` } },
).then((r) => r.json());

The MCP tools (amba_currencies_grant, etc.) are for the coding agent provisioning your project — at runtime, call the REST endpoints directly. Don't POST JSON-RPC from inside a function.

Example: read/write an arbitrary collection from a function

The config example above hits a specific collection (configs), but the same shape works against any collection in your project — swap the name in the path. Rows live under /v1/admin/projects/${env.AMBA_PROJECT_ID}/collections/{name}/rows:

// Insert a row into an arbitrary collection (here, `audit_log`).
const created = await fetch(
  `${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/collections/audit_log/rows`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ action: 'export_ran', actor: userId }),
  },
).then((r) => r.json());
 
// Patch a single row by id. The admin PATCH body wraps fields in a `set` envelope.
await fetch(
  `${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/collections/audit_log/rows/${created.data.id}`,
  {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ set: { status: 'reconciled' } }),
  },
);

To insert many rows in one atomic call, POST to the bulk path .../collections/{name}/rows:batch (up to 500 rows per call) instead of looping single inserts — the same endpoint the SDK and MCP seeding tools use. The admin surface is not auto-scoped to an end user the way the client SDK is: a function holding the project token can read and write any row, so it's the right place for cross-user bookkeeping.

Uploading media from a function

When STORAGE is provisioned for your project (see the env table above), a function can write a file straight into media storage with env.STORAGE.put(key, body, options?). The stored object is then served from your project's media CDN host — no separate registration step.

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const { id, text } = await req.json();
 
    // Produce bytes however you like — synthesize audio, render an image,
    // transform an upload. Here we just stand in for the produced bytes.
    const bytes: ArrayBuffer = await synthesizeSpeech(text, env);
 
    const key = `audio/${id}.mp3`;
    await env.STORAGE.put(key, bytes, {
      httpMetadata: { contentType: 'audio/mpeg' },
    });
 
    // The object is now served from the project's media CDN host.
    return Response.json({ key });
  },
};

put accepts a string, an ArrayBuffer, a typed array, or a ReadableStream as the body. Pass httpMetadata.contentType so the object is served with the right type. The companion methods are env.STORAGE.get(key), env.STORAGE.delete(key), and env.STORAGE.list({ prefix }) — use get to read an object back inside the function and list to enumerate by prefix.

Signed-header identity

When the edge router dispatches a request to a function on *.fn.amba.host, it strips and re-injects these headers (HMAC-signed against env.EDGE_HEADER_SIGNING_SECRET):

HeaderValue
X-Amba-Project-IdThis function's project
X-Amba-User-IdSigned-in user id, or empty string when unauthenticated
X-Amba-Developer-IdDeveloper id when called with an admin PAT, else empty
X-Amba-Function-NameThe function name (post-prefix-strip)
X-Amba-Request-IdTrace id
X-Amba-TimestampSigning time (Unix seconds)
X-Amba-SignatureHMAC-SHA256 over the canonical concat of the above

To trust X-Amba-User-Id, verify the signature first. The canonical signing format lives in @layers/amba-api-middleware — copy that function into your bundle if you need verification (it's ~20 lines of crypto.subtle.importKey + verify).

Complete working example

A function that reads a config from the admin API + returns it. No extra setup needed — AMBA_INTERNAL_TOKEN is injected at deploy.

export default {
  async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
    const res = await fetch(`${env.AMBA_API_URL}/v1/admin/projects/${env.AMBA_PROJECT_ID}/config`, {
      headers: { Authorization: `Bearer ${env.AMBA_INTERNAL_TOKEN}` },
    });
    if (!res.ok) {
      return Response.json({ error: 'upstream', status: res.status }, { status: 502 });
    }
    const { data } = await res.json();
    const cfg = data.find((c: { key: string }) => c.key === 'launch_config');
    return Response.json({ launch_config: cfg?.value ?? null });
  },
};
 
interface Env {
  AMBA_API_URL: string;
  AMBA_PROJECT_ID: string;
  AMBA_INTERNAL_TOKEN: string;
  EDGE_HEADER_SIGNING_SECRET: string;
  AMBA_AI_GATEWAY_URL: string;
  STORAGE?: unknown;
}

Deploy: amba functions deploy ./functions/get_launch_state.ts. The CLI bundles via esbuild (--bundle --format=esm --platform=browser --target=es2022) and POSTs the result.

Bundle constraints

  • Format: ESM, single-file, default-exports the handler object. The CLI also accepts the legacy event-listener addEventListener('fetch', ...) shape but it's not emitted by the bundler.
  • Bundle size cap: 8 MB compressed, hard-rejected client-side before upload.
  • No external imports: the CLI bundles every reachable import into the output. There is no runtime-provided npm stdlib to externalize against — every import must resolve at bundle time.

Common upload-time errors

The deploy API returns { "error": { "code": "DEPLOY_REJECTED", "details": { "upstream_status": 400 } } } when the runtime rejects the bundle. The most common causes:

Symptom in your bundleWhy the runtime rejectsFix
import { x } from 'some-package' left externalThe runtime has no module loader — every import must resolve at build timeDrop --external:some-package; reinstall the import; or inline the dep yourself
export default async function() { return new Response(...) }Missing the { async fetch } handler shapeUse export default { async fetch(req, env, ctx) { ... } }
Bundle larger than 8 MB compressedHard cap before uploadSplit the function, drop heavy deps, or move work to a chained function

If you need more detail than the typed code, reach out to support — the upstream's verbatim message is captured server-side but redacted from the client response intentionally.