Amba

Deploy a Function

Ship a TypeScript function to Amba's edge runtime with `amba functions deploy` — versioned bundles, optional per-function rate limits, and rollback from the console.

amba functions deploy <file> bundles your handler and ships it to your project's edge runtime. Each deploy is its own version; rollouts are gradual with auto-rollback on a 5xx breach.

First deploy

amba functions deploy ./functions/hello.ts
✓ Bundled hello (24 KB)
✓ Deployed hello @ version v_abc123
  Live at https://<project>.fn.amba.host/hello

Hit it:

curl 'https://<project>.fn.amba.host/hello?name=amba'
{"greeting":"hello, amba"}

Deploying a directory

<target> can be a single file or a directory. A directory is bundled from its entry — resolved by convention (index.ts / index.js / index.mjs) or named explicitly with --entry:

amba functions deploy ./functions/checkout                 # entry: index.ts
amba functions deploy ./functions/checkout --entry handler.ts --name checkout

This lets a function split across multiple local modules — the bundler follows the imports from the entry. The default name is the directory leaf name.

Flags

  • --name <name> — function name (default: the file name without extension, or the directory leaf name). The function is served at https://<project>.fn.amba.host/<name>.
  • --entry <file> — entry file when the target is a directory (relative to it). Default: index.ts/js/mjs.
  • --dry-run — bundle and report size without uploading. Useful for CI to fail when a function approaches the size cap.
  • --rate-limit-window <60s|5m|1h> — per-function rate-limit window.
  • --rate-limit-max <int> — max requests per window.
  • --rate-limit-key <user_id|ip> — bucket key for the rate limit (user_id reads the signed-in user from the session token; ip buckets by client IP).

All three rate-limit flags must be set together, or all omitted (no rate limit). The edge runtime enforces the limit pre-dispatch — the handler never runs for rate-limited requests.

Rate limit example

amba functions deploy ./functions/checkout.ts \
  --rate-limit-window 60s \
  --rate-limit-max 5 \
  --rate-limit-key user_id

That allows each signed-in user 5 requests per minute against /checkout. The 6th in a window returns 429 Too Many Requests from the edge without invoking the handler.

Public functions (webhook receivers)

By default every function is private: callers must send your project's X-Api-Key header, so only your own app reaches it. That's the right default — keep it for anything only your app calls.

Some callers can't send a custom header. Third-party webhook senders (Stripe, RevenueCat, GitHub, Slack, …) POST to a fixed URL with their own headers and no way to add yours. For those, deploy the function as public so the runtime skips the API-key requirement:

amba functions deploy ./functions/on-webhook.ts --public
// MCP / agent equivalent:
// amba_functions_deploy({ project_id, name: "on-webhook", code, public: true })

You verify the signature. Public only skips Amba's API-key gate. It does not authenticate the caller for you. A public function is reachable by anyone on the internet, so you must verify the webhook's HMAC signature inside your handler before trusting the payload — exactly as the vendor's docs describe. Public also changes nothing else: your per-function rate limit still applies (set one — public endpoints need it more), and the request still runs inside your own project.

Server-to-server callers (your backend, a cron job, curl, default Python / Go HTTP clients) are accepted on function URLs regardless of --public; they just still need the X-Api-Key header unless the function is public.

Webhook-receiver example

// functions/on-webhook.ts — a Stripe webhook receiver.
export default {
  async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
    const signature = req.headers.get('Stripe-Signature') ?? '';
    const payload = await req.text();
 
    // Verify the signature BEFORE trusting anything in the body. Reject
    // unsigned / mis-signed requests — the function is public, so this is
    // your only authentication.
    const ok = await verifyStripeSignature(payload, signature, env.STRIPE_WEBHOOK_SECRET);
    if (!ok) {
      return new Response('invalid signature', { status: 401 });
    }
 
    const event = JSON.parse(payload);
    // ... handle the verified event ...
    return new Response('ok', { status: 200 });
  },
};
 
interface Env {
  STRIPE_WEBHOOK_SECRET: string; // amba secrets set STRIPE_WEBHOOK_SECRET whsec_... --function on-webhook
}
amba functions deploy ./functions/on-webhook.ts --public
# Register the printed URL in the vendor dashboard:
#   https://<project>.fn.amba.host/on-webhook

The --public flag is per-function and per-deploy — it travels with the deployment, so a rollback to a private version restores the API-key requirement. Deploy without --public to make a function private again.

Reading secrets in production

Secrets set with the CLI are injected as env.* bindings — accessible via the second parameter to fetch. A secret is project-wide by default (omit --function, every function sees it) or function-scoped (pass --function). You can set secrets before this function is deployed; the sync drains once the deploy lands, and deploy re-syncs at the end.

amba secrets set STRIPE_API_KEY sk_live_... --function checkout
amba secrets list
export default {
  async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
    const stripeKey = env.STRIPE_API_KEY;
    // ...
  },
};
 
interface Env {
  AMBA_API_URL: string;
  AMBA_PROJECT_ID: string;
  AMBA_INTERNAL_TOKEN: string;
  EDGE_HEADER_SIGNING_SECRET: string;
  AMBA_AI_GATEWAY_URL: string;
  STRIPE_API_KEY: string; // your custom secret
}

In local dev, the same key lives in .env.local and is passed to the handler the same way — see Local dev. For scoping (project-wide vs. one function) and the full lifecycle, see Secrets.

Rollback

Every deploy is a new version with a unique id. The console's Rollouts view lists active versions in flight and offers a one-click rollback per version. The CLI also exposes:

amba functions logs <name> --tail   # watch error rate live

If a deploy regresses, ramp the previous version back to 100% from the console Rollouts page.

Watching logs

amba functions logs hello --tail
amba functions logs hello --since 2026-05-15T00:00:00Z --json | jq 'select(.level == "error")'

--tail (alias --follow) streams new log events as they arrive. --json emits one NDJSON event per line for piping into jq, log-shipping tools, or local analysis.

When your function throws

If your handler throws an unhandled exception, Amba's edge returns a stable error envelope to the caller and your client SDK will surface it as a typed error. The response shape is:

HTTP/2 502
Content-Type: application/json
 
{
  "error": {
    "kind": "function_error",
    "code": "function_threw",
    "status": 502,
    "message": "Function threw an unhandled exception. Check your function logs for the stack trace.",
    "request_id": "r_..."
  }
}

Key things to know:

  • kind: "function_error" is the SDK-side discriminator. Client SDKs use it to decide not to retry — re-running your code would throw the same exception, so retries would just burn CPU and invocation budget.

  • No retry_after field. Absence is the explicit "do not retry" signal. If retry_after IS present on an error, the SDK retries after that many seconds (this is how rate_limited 429 and amba_router_unavailable 503 work — those are retryable; this is not).

  • Status 502, not 500. The edge router is a gateway between your caller and your handler; a 502 means "your handler returned an invalid response" (or threw). A 500 would point to a problem on Amba's side — different cause, different fix.

  • The exception message is NOT echoed in the response body. Throw messages can carry secrets or PII; the SDK and the caller see only the stable envelope above. Your full stack trace lives in your function's log stream. Pull the log window around the failure and scan for the exception:

    amba functions logs hello --since 2024-01-01T00:00:00Z --json

    To zero in on one request, pass --request-id — it returns only the events whose logs mention that request id (the r_... value returned to the caller). Works with --tail too:

    amba functions logs hello --request-id r_019e55ef1f46

If a caller is seeing function_threw and you didn't expect it, pull the log window around that time with amba functions logs and find the exception.

Common pitfalls

  • Bundle exceeds size capamba functions deploy --dry-run to see the bundle report. The heaviest deps are usually large NPM packages pulled in transitively; check the report for the top size contributors and prune.
  • Missing secret at runtime — confirm you ran amba secrets set for the project you deployed to. amba secrets list should show the name (values are never echoed).
  • First request slow — cold-starts hit after a long idle. The warm-path latency is much lower; in load tests, send a warming request before the burst.
  • Caller sees 502 function_threw — your handler threw an unhandled exception. See "When your function throws" above for the full contract. Pull the log window around the error time (amba functions logs <name> --since <iso-timestamp> --json) and match the exception in the output, or pass --request-id <r_...> to filter to that one request's events.

Next

On this page