Skip to content

Ship your first function

This guide starts from a project you have already deployed and ends at a function you can call from anywhere. The files it shows are the ones in codeberg.org/deploybase/functions-example, which you can also deploy as-is.

Open Billing in the dashboard. The functions card says Your team is in. when the grant is active. If it says the team has applied, we have not answered yet; if it offers to apply, nobody on the team has.

Without the grant everything below still works up to the HTTPS call: the function is bundled, registered and can be queued, but the hostname answers 404 and the build log says so. A team admin can apply at app.deploybase.eu/functions-beta.

Create a functions/ directory at the root of your project (inside the root directory if your project sets one) and add this file:

functions/hello.js
export default async function handler(request) {
const name = new URL(request.url).searchParams.get("name") ?? "world";
return new Response(JSON.stringify({ message: `Hello, ${name}!` }), {
headers: { "content-type": "application/json", "access-control-allow-origin": "*" },
});
}

The default export is the handler. It receives a standard Request and returns a standard Response; nothing here is specific to deploybase. The access-control-allow-origin header is there so a page on your site, which lives on a different hostname, can call the function from the browser. deploybase adds no CORS headers of its own.

A .ts file works the same way; types are stripped when the function is bundled, with no tsconfig and no type-check step. Files in a subdirectory of functions/ are shared code, never functions of their own, so functions/_lib/greeting.js can be imported by functions/hello.js and is bundled into it.

Commit and push. Functions are discovered and deployed on production deploys only: a push to the production branch, or a promotion. A preview branch builds the site as usual and skips the functions/ directory entirely.

The build log shows the bundle and the registration:

[functions] Bundled 1 function(s): hello (1208 bytes)
⚡ Registered function "hello" (http, 60s timeout)

If the team is not in the beta yet, the second line is replaced by a warning that names the function and says it was not deployed as an HTTPS endpoint. Fix access, and the function comes up within a few minutes without another deploy.

Every function gets its own hostname: https://{name}-{subdomain}.fn.deploybase.eu. {name} is the file name without its extension, and {subdomain} is your project’s subdomain, the first label of your {subdomain}.sites.deploybase.eu address, shown in Settings. The examples below use example-site in its place.

Terminal window
curl "https://hello-example-site.fn.deploybase.eu/?name=Ada"
{"message":"Hello, Ada!"}

The first call after a quiet spell takes a few seconds. Functions scale to zero when nobody is calling, and that first request starts an instance before it is answered, usually in 2 to 5 seconds. Calls after that are answered directly. If you see a header named x-keda-http-cold-start in the reply, that is the component that scales instances up and down reporting whether this call paid for a start.

The same file can run in the background. A POST to the queued address answers straight away with 202 Accepted and a URL to poll:

Terminal window
curl -X POST "https://api.deploybase.eu/api/v1/public/functions/example-site/hello?name=Ada"
{
"data": {
"invocation_id": "01234567-89ab-cdef-0123-456789abcdef",
"status": "pending",
"poll_url": "/api/v1/public/functions/example-site/hello/invocations/01234567-89ab-cdef-0123-456789abcdef"
}
}

Poll https://api.deploybase.eu plus the poll_url until status is no longer pending or running. A finished call carries the function’s status code, headers and body, with the body base64 encoded:

Terminal window
curl "https://api.deploybase.eu/api/v1/public/functions/example-site/hello/invocations/01234567-89ab-cdef-0123-456789abcdef"
{
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"status": "completed",
"response_status": 200,
"response_headers": {
"access-control-allow-origin": "*",
"content-type": "application/json"
},
"response_body": "eyJtZXNzYWdlIjoiSGVsbG8sIEFkYSEifQ==",
"started_at": "2026-09-14T09:20:28Z",
"finished_at": "2026-09-14T09:20:36Z"
}
}

Decoding response_body gives {"message":"Hello, Ada!"}. The queued door hands your handler the same Request the HTTPS door would have: the function’s own URL with the query string, the headers you sent, and the body. A queued call takes several seconds end to end because each one starts a fresh container, and the result stays available for 7 days. The reference has the limits and the error messages.

Open the project’s Functions tab. Each function has a page with both doors: Call it shows the HTTPS address and whether an instance is serving, Queue it shows the queued address, and both have a copy-as-curl button. Below them, Invocations lists every queued call with its status, response and anything the function wrote to the console. HTTPS calls are not listed there yet.

That is the whole loop: a file, a push, a URL.

You see Likely cause What to do
No [functions] line in the build log The deploy was a preview, or functions/ is not at the project root Push to the production branch; move the directory
Bundled 1 function(s) but your function is missing The file is in a subdirectory Move it to the top of functions/. Subdirectories are shared code
[functions] Build failed: invalid function name The file name has uppercase letters, underscores or other characters outside a-z, 0-9 and - Rename the file
[functions] Build failed A bundle is over 900 KB, an import could not be resolved, or two files share a name Trim dependencies; the log names the file
404 at the hostname The team is not in the beta, the function is not deployed yet, or {name}-{subdomain} is longer than 63 characters Check the billing card and the build log; shorten the name
504 The handler took longer than 60 seconds See timeouts. The code keeps running after the 504
500 with no detail The handler threw, or returned something that is not a Response The reply carries no detail on purpose. Reproduce with a queued call to read the console output
413 Request body over 10 MiB on the HTTPS door Send less, or stream from storage instead
Browser says the request was blocked by CORS The handler did not set access-control-allow-origin Add the header, as hello.js does. Keep browser calls to a plain GET with no custom headers so no preflight is sent

Example repository: codeberg.org/deploybase/functions-example. Verified on 14 September 2026 against deploybase v2026.9.16.