# Functions

Functions are serverless functions for your site: a light backend for the few things a static page cannot
do on its own — receive a webhook, call an API with a key the browser never sees, handle a form your way.
They run per request on the edge that serves your pages, so there is no server to keep running. They keep
no data of their own: there is no database beside them, and nothing runs on a schedule or in the
background.

Put a folder named `_functions` at the top level of what you publish. Each JavaScript file in it answers
the path it is named after, on your site's own address:

```text
my-site/
├─ index.html
└─ _functions/
   ├─ api/
   │  ├─ hello.js          → /api/hello
   │  └─ users/[id].js     → /api/users/42, /api/users/abc, …
   └─ _lib/
      └─ db.js             a helper, not a route
```

```js
// _functions/api/hello.js
export async function onRequestPost({ request, env, params }) {
  const { name } = await request.json();
  return Response.json({ greeting: `Hello, ${name}` });
}
```

Functions publish with the rest of the folder, in the same version: rolling back to an earlier version
brings back its functions too, and a version's pages and its code never disagree. Your code itself is
never served — `/_functions/api/hello.js` answers 404.

Functions run only on the plans that include them — [Pricing](https://dropto.run/pricing) lists which, and
when no plan does yet, it says nothing about them. On a plan without them, a folder with `_functions` still publishes:
the files in it are left out of what is served, the publish says so with a warning, and the paths answer
404 until you publish again on a plan that has them.

## How a file becomes a route

The path of a file under `_functions`, without its extension, is the route.

| File | Answers |
| --- | --- |
| `api/hello.js` | `/api/hello` |
| `api/index.js` | `/api` |
| `api/users/[id].js` | `/api/users/` and one more segment, as `params.id` |
| `api/files/[[path]].js` | `/api/files` and everything below it, as `params.path`, an array of segments |

- A **file or folder starting with `_`** is a helper: it can be imported by a route, and answers nothing
  itself. Names starting with `__` are reserved.
- Names use letters, digits, `.`, `_` and `-`; a parameter's name starts with a letter.
- The files the site serves are `.js` or `.mjs` modules. TypeScript works when you publish with the CLI,
  which compiles it (see [below](#npm-packages-and-typescript)).
- When two routes could both match a path, the more specific one wins: a fixed segment before `[id]`,
  `[id]` before `[[path]]`.
- **A route wins over a file and over a `_redirects` rule** at the same path. A proxy rule in
  `_redirects` (see [Calling your own API](/docs/redirects#calling-your-own-api)) may not share a path
  with a route: the publish is refused and names both.
- Paths under `/__d2r/` belong to the platform and never reach a function.

A version may hold up to 20 routes and 100 files under `_functions`, 5 MB in all.

## What a function receives

Export one function per method — `onRequestGet`, `onRequestPost`, `onRequestPut`, `onRequestPatch`,
`onRequestDelete`, `onRequestHead`, `onRequestOptions` — or `onRequest` for every method. A method with no
handler answers 405 with an `Allow` header. Each is called with one object:

| Field | What it is |
| --- | --- |
| `request` | The visitor's [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) |
| `env` | The site's [variables](#keys-and-settings), each a string |
| `params` | What the route's `[name]` and `[[name]]` segments matched |
| `clientIp` | The visitor's address — an IPv6 address cut to its `/64` |
| `waitUntil(promise)` | Keeps work going after the answer is sent, such as a log write |

It returns a `Response`. The code is a standard ES module running on Cloudflare Workers, so `fetch`,
`crypto.subtle`, `URL`, `TextEncoder`, `Response.json` and the other web APIs are there. Node's built-in
modules (`node:fs`, `node:crypto`, …) are not.

The request is rebuilt on the way in, so your code never sees the platform's own cookies, Cloudflare's
`cf` object, or headers that name the visitor's network (`cf-connecting-ip`, `x-forwarded-for` and the
like) — `clientIp` is the one thing it knows about who called.

## Keys and settings

A function reads its keys from `env`, never from the files you publish. Set them in the site's
**Functions** tab, with the CLI, or with the MCP server's `update_site`:

```bash
drop2run set env OPENAI_API_KEY --site my-site      # typed at a hidden prompt
printf %s "$OPENAI_API_KEY" | drop2run set env OPENAI_API_KEY --site my-site
drop2run unset env OPENAI_API_KEY --site my-site
```

- **A value is never shown again.** It is encrypted when you save it; the dashboard and `drop2run info`
  show its name and last four characters.
- **A change needs no new publish.** It reaches every route within about a minute. Code that copies `env`
  into a variable at the top of a module keeps the old value until that copy is replaced, so read
  `env` inside the handler.
- Names are uppercase letters, digits and `_`, starting with a letter or `_`. A site holds up to 50
  variables, each up to 5 KB, 40 KB in all.
- Removing a variable always works, on any plan — a key can always be taken back.

## npm packages and TypeScript

What you drop in the dashboard is published as it is, and the platform has no build step: a route that
imports an npm package by name (`import Stripe from "stripe"`) fails when it first loads. Imports between
your own files under `_functions` work either way.

`drop2run deploy` bundles each route for you: one module per route with everything it imports — your
helpers, npm packages from the folder's `node_modules`, TypeScript compiled to JavaScript. Run
`npm install` in the folder first. It picks a package's Worker or browser build, since Node's built-ins
are not available, and says which file and line it could not bundle if one fails. `--no-bundle` publishes
`_functions` as it is.

## What a response may carry

Your function sets its own status, body and headers, including CORS headers and `Content-Type`. A few are
changed on the way out, on every answer:

- `Cache-Control` is always `no-store`: an answer from a function is never cached by the edge or by the
  browser, so a fix you publish is the next answer.
- `X-Content-Type-Options: nosniff` and the site's `X-Robots-Tag` are always added.
- Headers that would act beyond your site are dropped: `Clear-Site-Data`, `WWW-Authenticate`,
  `Proxy-Authenticate`, `Origin-Agent-Cluster`, `NEL`, `Report-To`, `Reporting-Endpoints`, `Set-Login`,
  `Alt-Svc`, and anything starting `cf-` or `x-d2r-`.
- A `Set-Cookie` keeps its name, value, `Path`, `Expires`, `Max-Age`, `Secure`, `HttpOnly`, `SameSite` and
  `Partitioned`. `Domain` is always dropped, so a cookie belongs to the exact host it was set on. Name a
  session cookie with the `__Host-` prefix: on `*.dropto.live`, another site cannot overwrite it.
- An answer may be up to 25 MB; a longer one is cut off.

## Who can call a function

Anybody who can open the site. A route on a public site answers every visitor and every script, not only
your pages, so check what a request asks for before acting on it.

- **Password and invite-only come first.** On a site behind a password or invite-only, a call without the
  visitor's access answers as any page would. A webhook from a service outside cannot sign in, so put
  routes that receive webhooks on a public site.
- **Pages of other sites cannot call in with your visitors' cookies.** On `*.dropto.live`, a call made from
  a page of another `dropto.live` site is refused with 403. On a site behind a password or invite-only,
  every call from a page of another site is refused — including the browser's return from a sign-in
  provider — so a route that receives one belongs on a public site too. On your own domain, calls between
  your own subdomains work.
- **Calls with no browser behind them** — a webhook, a server, `curl` — have neither header and go
  through.

## Calling other services

`fetch` reaches the public internet over `http` and `https`, on ports 80 and 443. To keep one site from
turning the platform on another, a function cannot reach:

- private, loopback and other non-public addresses, written as numbers in any form;
- `localhost`, `dropto.run`, `dropto.live` and their subdomains — another site on the platform included
  — and Cloudflare's `workers.dev`, `r2.dev` and `cfargotunnel.com`;
- raw TCP sockets.

Redirects are not followed for you: a `3xx` comes back to your code, and following it is another `fetch`.
A call to a function may make up to 10 `fetch` calls.

## Limits and errors

| Limit | |
| --- | --- |
| CPU time per call | 50 ms unless your plan says otherwise — the time your code computes, not the time it waits on `fetch` |
| Request body | 10 MB |
| Response body | 25 MB |
| Waiting for the first byte | 30 seconds |
| Silence between two chunks | 60 seconds |
| A whole call | 5 minutes |
| `fetch` calls per call | 10 |

Calls count against your plan's monthly allowance, every site of the account together. Past it, every
function of the account answers 429 until the month turns; the pages keep serving. Anybody who can reach
a route can spend that allowance, so a public route that does little should still do little for an
unexpected caller. A burst on one site is also cut off by a per-site limit, which answers 429 with
`Retry-After`.

When something goes wrong, the answer is JSON naming a fixed code and never your code's message, which
could hold a key:

```json
{ "error": "function_error", "source": "drop2run-functions" }
```

| Status | `error` | Means |
| --- | --- | --- |
| 404 | `function_not_found` | No route answers this path in the live version |
| 405 | `method_not_allowed` | The route exports no handler for this method |
| 413 | `request_too_large` | The request body is over 10 MB |
| 429 | `rate_limited`, `functions_suspended` | Too many calls in a short time, or the month's allowance is spent |
| 500 | `function_error` | Your code threw, failed to load, ran out of CPU time, or did not return a `Response` |
| 502 | `function_bad_response` | The `Response` had a status below 200, or opened a WebSocket |
| 502 | `functions_unavailable` | The platform could not reach the functions service; try again |
| 504 | `function_timeout` | No answer within the times above |

There is no log of your code's `console.log` yet. To see what a route does, return it in the answer while
you work on it.

## Previews and rollbacks

Functions run only on the site's live address and its custom domains. A preview link of another version
never runs functions — its function paths answer 404 — so test a change by publishing it, and roll back
from **Versions** if it is wrong. Rolling back changes the routes with the pages.
