dropto.run docs
Guides

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:

Code
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
JavascriptCode
// _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 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.

FileAnswers
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).
  • 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) 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:

FieldWhat it is
requestThe visitor's Request
envThe site's variables, each a string
paramsWhat the route's [name] and [[name]] segments matched
clientIpThe 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:

TerminalCode
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 call50 ms unless your plan says otherwise — the time your code computes, not the time it waits on fetch
Request body10 MB
Response body25 MB
Waiting for the first byte30 seconds
Silence between two chunks60 seconds
A whole call5 minutes
fetch calls per call10

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:

JSONCode
{ "error": "function_error", "source": "drop2run-functions" }
StatuserrorMeans
404function_not_foundNo route answers this path in the live version
405method_not_allowedThe route exports no handler for this method
413request_too_largeThe request body is over 10 MB
429rate_limited, functions_suspendedToo many calls in a short time, or the month's allowance is spent
500function_errorYour code threw, failed to load, ran out of CPU time, or did not return a Response
502function_bad_responseThe Response had a status below 200, or opened a WebSocket
502functions_unavailableThe platform could not reach the functions service; try again
504function_timeoutNo 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.

Last modified on