Build
Functions
A function is one TypeScript or JavaScript file that answers HTTP requests. It runs on Deno next to your app, with your app's env vars and a secret key already in its environment.
Write a function
Export a default handler that takes a Request and returns a Response:
cat > hello.ts <<'EOF'
export default async (req: Request): Promise<Response> => {
const body = req.method === "POST" ? await req.json().catch(() => ({})) : {};
const greeting = Deno.env.get("GREETING") ?? "Hello";
return Response.json({ message: `${greeting}, ${body.name ?? "world"}` });
};
EOFOr use Deno.serve, which is handy when you already have Deno code:
cat > whoami.ts <<'EOF'
Deno.serve((req) => {
return Response.json({
user_id: req.headers.get("Berth-User-Id"),
email: req.headers.get("Berth-User-Email"),
});
});
EOFImports
A function may import packages from npm:, jsr:, and https:// URLs such as esm.sh. It may not import other local files, because only the one file is deployed. If your code is spread over several files, bundle it into one first (for example with esbuild).
cat > digest.ts <<'EOF'
import { encodeHex } from "jsr:@std/encoding/hex";
export default async (req: Request) => {
const data = new Uint8Array(await req.arrayBuffer());
const hash = await crypto.subtle.digest("SHA-256", data);
return Response.json({ sha256: encodeHex(hash) });
};
EOFThe other two forms look like this:
import { Chess } from "npm:chess.js";
import { nanoid } from "https://esm.sh/nanoid";Deploy
hello.ts reads GREETING, which is not set yet. An unset env var reads as undefined, so the ?? "Hello" fallback applies.
berth functions deploy --app "$APP" hello hello.ts --verify key
berth functions deploy --app "$APP" whoami whoami.ts --verify user --timeout-ms 5000 --memory-mb 128
berth functions deploy --app "$APP" digest digest.ts
berth functions list --app "$APP"Deploying the same name again replaces it. The HTTP form is a PUT with the source as a JSON string:
jq -n --rawfile source hello.ts '{source: $source, verify: "key", timeout_ms: 10000}' | \
curl -sS -X PUT https://api.atberth.com/v1/apps/$APP/functions/hello/deployment \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
--data-binary @-Verify modes
| verify | Who may call it |
|---|---|
key | anyone with the app's publishable or secret key (the default) |
user | signed-in users only; the function gets Berth-User-Id and Berth-User-Email |
none | anyone on the internet, for example a payment provider's webhook. Check signatures yourself. |
Invoke
A function answers any method at /v1/apps/{app}/functions/{name}, and its response is passed through as is.
curl -sS -X POST https://api.atberth.com/v1/apps/$APP/functions/hello \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Ada"}'
berth functions invoke --app "$APP" hello --data '{"name":"Grace"}'
berth functions invoke --app "$APP" digest --data 'hello' --content-type text/plain | grep 2cf24dbaA user function needs a signed-in user's token:
ACCESS_TOKEN=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/signup \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery"}' | jq -er .access_token)
curl -sS https://api.atberth.com/v1/apps/$APP/functions/whoami \
-H "apikey: $PUBLISHABLE_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Env vars
Set secrets such as third party API keys as env vars. Values are write only: the API lists names and updated_at, never values. Names are uppercase letters, digits, and underscores, and may not start with BERTH_ or DENO_.
berth env set --app "$APP" GREETING=Welcome
berth env list --app "$APP"
berth functions invoke --app "$APP" hello --data '{"name":"Ada"}'curl -sS -X PUT https://api.atberth.com/v1/apps/$APP/env/GREETING \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"value":"Hi"}'
berth env unset --app "$APP" GREETINGEvery function also gets these, set by Berth:
| Name | Value |
|---|---|
BERTH_API_URL | the API base URL |
BERTH_APP | the app name |
BERTH_SECRET_KEY | a secret key for this app, so the function can call the API with full access |
A function that counts notes with the injected key. The key stays on the server:
berth tables create --app "$APP" notes title:text:notnull
berth rows add --app "$APP" notes title="From a function"
cat > count_notes.ts <<'EOF'
export default async () => {
const base = Deno.env.get("BERTH_API_URL");
const app = Deno.env.get("BERTH_APP");
const res = await fetch(`${base}/apps/${app}/tables/notes/rows?count=exact&limit=1`, {
headers: { Authorization: `Bearer ${Deno.env.get("BERTH_SECRET_KEY")}` },
});
const { count } = await res.json();
return Response.json({ notes: count });
};
EOF
berth functions deploy --app "$APP" count_notes count_notes.ts
berth functions invoke --app "$APP" count_notes --method GETSchedules
Give a function a cron schedule and Berth runs it on its own: nightly cleanups, reminders, syncing from another API. A schedule is five cron fields in UTC (minute hour day month weekday), or one of @hourly, @daily, @weekly, @monthly.
Each minute that matches, Berth sends the function a POST with the body {"scheduled_at": "2026-09-22T10:15:00Z"} and these headers:
| Header | Value |
|---|---|
Berth-Key-Type | schedule. Berth sets this header itself on every call (publishable, secret, user, or none for other callers), so a caller cannot fake it. |
Berth-Schedule | the cron expression, for your logs. Do not use it to decide who called; check Berth-Key-Type. |
Scheduled runs do not go through the function's verify mode, and they show up in the function's logs like any other run.
cat > cleanup.ts <<'EOF'
export default async (req: Request) => {
if (req.headers.get("Berth-Key-Type") !== "schedule") {
return new Response("Only the scheduler runs this.", { status: 403 });
}
const { scheduled_at } = await req.json();
const base = Deno.env.get("BERTH_API_URL");
const app = Deno.env.get("BERTH_APP");
const res = await fetch(`${base}/apps/${app}/tables/notes/rows?title=eq.expired`, {
method: "DELETE",
headers: { Authorization: `Bearer ${Deno.env.get("BERTH_SECRET_KEY")}` },
});
return Response.json({ scheduled_at, cleaned: await res.json() });
};
EOF
berth functions deploy --app "$APP" cleanup cleanup.ts --schedule "*/15 * * * *"The deployment response includes "schedule". A redeploy that leaves the schedule out keeps it. --no-schedule, or "schedule": null over HTTP, removes it.
berth functions deploy --app "$APP" cleanup cleanup.ts --json | jq -e '.function.schedule == "*/15 * * * *"'
jq -n --rawfile source cleanup.ts '{source: $source, schedule: "@daily"}' | \
curl -sS -X PUT https://api.atberth.com/v1/apps/$APP/functions/cleanup/deployment \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
--data-binary @- | jq -e '.function.schedule == "@daily"'
jq -n --rawfile source cleanup.ts '{source: $source, schedule: null}' | \
curl -sS -X PUT https://api.atberth.com/v1/apps/$APP/functions/cleanup/deployment \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
--data-binary @- | jq -e '.function.schedule == null'
berth functions deploy --app "$APP" cleanup cleanup.ts --schedule @hourly
berth functions deploy --app "$APP" cleanup cleanup.ts --no-schedule --json | jq -e '.function.schedule == null'A bad expression is rejected with 400 invalid_body:
jq -n --rawfile source cleanup.ts '{source: $source, schedule: "every day"}' | \
curl -sS -X PUT https://api.atberth.com/v1/apps/$APP/functions/cleanup/deployment \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
--data-binary @- | jq -e '.error == "invalid_body"'Sandbox rules
- Network: the public internet only. No localhost, no private ranges, no cloud metadata addresses.
- No file system and no subprocesses.
- Only this app's env vars are visible. A name that is not set reads as
undefined; the host's own variables are never reachable. - Imports from
npm:,jsr:, andhttps://URLs work. Relative imports of local files do not. - Timeout: 10 seconds by default, 30 at most (
--timeout-ms). Past it the caller gets 504timeout. - Heap: 128 MB by default, 256 MB at most (
--memory-mb). - Four runs at once per app, 20 functions per app, 1 MB of source each.
- A crash or an empty response returns 502
function_error.
Logs
Each run of a function is logged. Read the recent entries:
berth functions logs --app "$APP" hello
curl -sS "https://api.atberth.com/v1/apps/$APP/functions/hello/logs?limit=20" \
-H "Authorization: Bearer $SECRET_KEY"Show and delete
berth functions show --app "$APP" hello
berth functions delete --app "$APP" whoami