Skip to content
esc

Search pages and sections. Try a tool name, an error code, a route.

↑↓ navigate↵ openesc close

Guides · Guides

Write an edge function

Server-side code that runs on its own subdomain of your project, calls your project's API with a secret key nobody ever sees, and reads the secrets you set — once for a project, or once for your whole account.

What a function is#

One bundled ES module with a default export, deployed by your agent, served at https://proj-<id>-fn-<slug>.manybases.com. It has no direct access to the database or the file store: everything it reads or writes goes over HTTP to its project's API, carried by its own secret key, and passes through the same checks as any other request.

Two environment variables are injected at deployment:

  • MANYBASES_URL — the project's API, https://proj-<id>.manybases.com/v1;
  • MANYBASES_SECRET_KEY — the function's own secret key. Minted at deployment, injected into the script, never returned to anyone — not to the agent, not to you.
js
export default {
  async fetch(request, env) {
    const res = await fetch(`${env.MANYBASES_URL}/tables/checkins/rows?limit=200`, {
      headers: { Authorization: `Bearer ${env.MANYBASES_SECRET_KEY}` },
    });
    const { data, error } = await res.json();
    if (error) return Response.json({ error }, { status: res.status });
    return Response.json({ count: data.length });
  },
};

The key ignores the access presets, like any sk_: a function sees every row. Rows it writes have owner_id: null.

Deploying#

The agent bundles the code on your machine (one module, everything inlined — no file paths, no imports left) and calls deploy_function with the slug and the source. The slug becomes the subdomain: 1 to 28 characters, a-z0-9, hyphens inside only; secrets is reserved. Redeploying the same slug replaces the function and rotates its key. Optional vars carry plain-text values (reserved names are refused by name, and so is a value shaped like a provider key — keys are secrets); optional limits set the per-invocation budget — by default 10 000 ms of CPU and 100 subrequests, beyond which an invocation answers 503; 10 000 ms of CPU is also the maximum, and a higher cpu_ms is refused with a 400. The module is capped at 1 MB. No human is required.

Nothing is uploaded from the dashboard; the Functions screen lists what the agent deployed.

Secrets#

Your code will need keys of other services. The doctrine: the agent writes the code, the human types the value, and nobody reads it back.

  1. Call list_secrets first: it returns the names of the project's secrets, with their dates — each one is what env.NAME holds in every function of the project. Never a value, never a digest.
  2. In the code, read the value as env.NAME. Never put a key in vars or in the code: a vars value shaped like a known provider key (sk-ant-, sk-proj-, sk_live_, rk_live_, re_) is refused with a 400 that names the variable, never the value. It is a belt, not a guarantee — a key of an unknown format passes, and nothing stops code from holding one.
  3. Deploy. Until the value is set, env.NAME is undefined at run time — deployment never fails for a missing secret.
  4. If a name you need is missing, ask the human to set the value on the dashboard: in the project's Manage → Secrets, or once for every project on the account's Secrets screen. A project secret reaches every function of the project within seconds; a shared one reaches the functions already running a few at a time, and its screen says how far it went. Either way, a function deployed afterwards starts with it, and both survive redeployments.
  5. Check once with list_function_secrets: it returns the names actually present on this function's script — the edge truth — and never a value. Don't poll it: each call costs a Cloudflare administration request.

Names follow [A-Za-z_][A-Za-z0-9_]{0,63}; the MANYBASES_ prefix is reserved. Values are capped at 5 KB, encrypted at rest, and never returned by any route. A function deployed while a value is being set may serve the previous value until the human saves again or the function is redeployed.

Called from a browser#

A function often serves the front end of an app that lives on another origin — a site on manybases.online, the function on manybases.com. The platform answers every preflight itself, and adds its CORS headers to a function's response when the code did not set Access-Control-Allow-Origin — on errors as on successes, so the front end can read them. A function that sets its own header keeps its decision, byte for byte, and a WebSocket upgrade passes through untouched. Because the platform answers the preflight, a browser may only call with GET, POST, PATCH or DELETE and the headers authorization, x-session, content-type and idempotency-key — no PUT, no custom header. Access-Control-Allow-Credentials is never sent: a browser never lets another origin read a response obtained with cookies.

A result many visitors read#

A report or a summary that every visitor reads should not call a paid API once per visitor. Store the result in a table the front end reads with the publishable key, created with the lecture_publique preset — anyone reads it, and signed-in users of the app can add rows; partage would be read by signed-in users only. The function's own key ignores presets, so it reads and writes the table either way — and because signed-in users can add rows too, it reads its own row back by the id it wrote, never "the latest row".

Serve the stored copy while it is fresh, and take a lease before recomputing: POST ${env.MANYBASES_URL}/sql with an UPDATE whose WHERE matches the row's id and a lease time that has passed. Compute the times in JavaScript — the SQL grammar has no now(). A row_count of 1 means this caller holds the lease; 0 means another one is already recomputing. So one caller, not every visitor, pays the upstream API.

Reading what a function did#

Execution output — console.log, a CPU overrun — is not readable through any tool or screen yet. What is captured is every request the function made to its project's API, in the project's request log under the function's key: the function's card carries log_key_ids, every generation of its key (redeploying is a rotation), and get_logs with key_id: "in.(…)" returns them. The dashboard's Logs tab of the function does the same. A function whose code throws answers 500 with a message saying the deployed code failed.

Rotating and destroying#

  • rotate_function_secret mints a new key and places it without re-uploading the code. The old key dies with no grace; warm instances can carry it for a few seconds and see transient 401s on their data calls.
  • delete_function destroys the script and every generation of its key. No human required, idempotent. From its response on, the function no longer runs: its address answers 404, even to a visitor who keeps calling it. The project's secrets survive, and redeploying the slug brings it back with them — no need to wait.
  • The function's key also appears in the project's key list as fn:<slug>: revoking it there is the finest emergency stop; rotating it there is refused — use rotate_function_secret.

Call limits#

Every function counts its calls before its code runs: by default 60 a minute and 300 a UTC day from one address, and 3,000 a UTC day in total. Beyond a limit the call answers 429 with a Retry-After header, and the provider your code calls bills nothing. Handle it in the front end — tell the person to try again in a moment, never retry in a loop. If the function calls a paid API, ask yourself how many calls a day you are willing to pay for, and deploy with that figure: rate_limit: { per_function_per_day: 200 }. If a busy app reaches a default, raise it the same way. list_functions shows the limits in force and the day's calls.

Bounds#

1 MB of module, 10 000 ms of CPU and 100 subrequests per invocation by default, 10 000 ms of CPU at most, 5 KB per secret value. Nothing caps the number of functions of an account. A function carries at most 126 variables of yours — vars, project secrets and shared secrets together — and a project holds at most 126 secret names. Calls: 60 a minute and 300 a day per address, 3,000 a day per function, unless you set others.