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
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.*:
| Binding | Type | Always set | What it is |
|---|---|---|---|
AMBA_API_URL | string | yes | API root (e.g. https://api.amba.dev). |
AMBA_PROJECT_ID | string | yes | This project's id |
AMBA_INTERNAL_TOKEN | string (secret) | yes | Project-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_SECRET | string (secret) | yes | HMAC secret for verifying signed inbound edge headers (X-Amba-User-Id, etc.) |
AMBA_AI_GATEWAY_URL | string | yes | AI proxy URL |
STORAGE | object-storage | only when storage provisioned | Per-project object-storage handle (.get/.put/.delete/.list) |
<SECRET_NAME> | string | when set | Custom 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:
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/projectsto create a new project,GET /v1/admin/projectsto 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:
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.
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:
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.
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):
| Header | Value |
|---|---|
X-Amba-Project-Id | This function's project |
X-Amba-User-Id | Signed-in user id, or empty string when unauthenticated |
X-Amba-Developer-Id | Developer id when called with an admin PAT, else empty |
X-Amba-Function-Name | The function name (post-prefix-strip) |
X-Amba-Request-Id | Trace id |
X-Amba-Timestamp | Signing time (Unix seconds) |
X-Amba-Signature | HMAC-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.
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
importinto the output. There is no runtime-provided npm stdlib to externalize against — everyimportmust 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 bundle | Why the runtime rejects | Fix |
|---|---|---|
import { x } from 'some-package' left external | The runtime has no module loader — every import must resolve at build time | Drop --external:some-package; reinstall the import; or inline the dep yourself |
export default async function() { return new Response(...) } | Missing the { async fetch } handler shape | Use export default { async fetch(req, env, ctx) { ... } } |
| Bundle larger than 8 MB compressed | Hard cap before upload | Split 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.