zerolith.ioChecking…
Documentation

Docs

Everything you need to deploy and call a function: the handler contract, HTTP invocation, environment variables, quotas, metrics and agent driving. Every example matches how the platform actually behaves.

Quickstart

  1. Create an account and verify your email address.
  2. Deploy a function from the dashboard. A working sample is prefilled; pick Python or Node.js.
  3. Try it from its page with the "Test the function" button.
  4. Create an API key to call it from outside (public functions need no key).

The free signup credit easily covers these first steps: an idle function costs nothing.

The handler contract

A function is a single file exposing a handler. It receives a request object and returns the response: a string, a JSON object, or a (status, body, headers) tuple.

python
# main.py — handler: main.handler
def handler(request):
    # request.method   -> "GET", "POST", ...
    # request.path     -> str
    # request.headers  -> dict[str, str]
    # request.query    -> dict[str, str]
    # request.body     -> bytes
    # request.json()   -> parsed JSON body
    name = request.query.get("name", "world")
    return {"message": f"hello, {name}"}

    # other return shapes:
    #   "text"                          -> 200 text/plain
    #   {"a": 1} | [1, 2]               -> 200 application/json
    #   (404, "not found")              -> (status, body)
    #   (201, body, {"X-My": "hdr"})    -> (status, body, headers)
node.js
// main.js — handler: main.handler
exports.handler = (request) => {
  // request.method   -> "GET", "POST", ...
  // request.path     -> string
  // request.headers  -> object (lower-cased names)
  // request.query    -> object (first value per key)
  // request.body     -> Buffer
  // request.json()   -> parsed JSON body
  const name = request.query.name || 'world';
  return { message: `hello, ${name}` };

  // other return shapes:
  //   'text'                          -> 200 text/plain
  //   { a: 1 }                        -> 200 application/json
  //   [404, 'not found']              -> [status, body]
  //   [201, body, { 'X-My': 'hdr' }]  -> [status, body, headers]
};

The form's "Handler" field is module.function (default main.handler). Runtimes already bundle the common libraries: requests, httpx, pydantic, PyYAML on Python; axios, lodash, zod, dayjs on Node.

Invoking a function

A private function is called with an API key (created in the app, shown once) passed in the Authorization header:

curl
curl -H "Authorization: Bearer $ZEROLITH_KEY" \
     "https://<function-url>?name=neo"

A public function is called without authentication, which is handy for webhooks:

curl
# public function — no key needed
curl "https://<function-url>?name=neo"

A presigned URL calls one private function with no key on the caller's side, until the date you set. Create one from the function page; it is shown once, and you can revoke it before its expiry:

curl
# presigned URL — one function, until its expiry date
curl "https://<function-url>?faas_token=faast_..."

The token authorizes that function and nothing else, not your account and not your other functions. Calls made through it are billed to you like any other invocation, and the owner gates still apply (a suspended function or an empty balance refuses it).

Authentication is checked at the edge, before your function even wakes, so an unauthorized call costs you nothing. All HTTP methods (GET, POST, PUT, PATCH, DELETE) and paths are forwarded to the handler.

Live examples

Four functions really deployed on the platform, public and scale-to-zero. The code shown is the actual handler, and the "Call it live" button really hits the public URL: what you see is the function's own answer, nothing is faked.

Nothing is invoked (or billed) until you click: that is also what lets these functions honestly sit at zero instances. The first call triggers a brief cold start; the next ones are served warm.

The examples repository collects these handlers and more, ready to deploy, with the commands that ship them: github.com/zerolith-faas/zerolith-examples

Environment variables

Define reusable variables (config or secret) at the account level, then attach them per function. They are injected as plain environment variables:

env
# Python                      # Node.js
import os                      process.env.MY_VAR
os.environ["MY_VAR"]

Everything is also driven through the API: create, list, update and delete your variables, then attach them to a function through its env_var_ids field:

curl
# create a secret — its value is write-only, it can never be read back
curl -X POST -H "Authorization: Bearer $JWT" \
     -d '{"name": "API_TOKEN", "kind": "secret", "value": "s3cr3t"}' \
     https://zerolith.io/api/env-vars

# attach it to a function (rolls a new revision)
curl -X PATCH -H "Authorization: Bearer $JWT" \
     -d '{"env_var_ids": ["<env-var-id>"]}' \
     https://zerolith.io/api/functions/<id>

Updating a variable's value automatically rolls a new revision of every function using it. Deleting a variable still attached to a function is refused (409) until it is detached.

Secret values are write-only: stored only in the cluster, never readable back through the API. The FAAS_ prefix is reserved for the platform.

Databases

Every account can provision a private database: network-accessible SQLite, replicated continuously to object storage. It lives in your isolated space, is never publicly exposed, and is reachable only by your own functions.

It sleeps just like a function. With no query for the configured idle window (10 minutes by default, adjustable from 6 seconds to 1 hour) the instance stops: you pay no compute at all, only the storage tier you hold, because your data sits in object storage and never moves. The next query wakes the database automatically and pays a few seconds of cold start, restore included. A longer idle window avoids paying that wake-up between two spaced-out requests. If you would rather have consistently flat latency, always-on keeps one instance up permanently, at the corresponding compute cost. Both settings can be changed at any time, without moving the data.

Attach the database to a function and the platform injects these two variables into its pod. The token is never displayed, it is mounted from a write-only secret.

env
# Python                                  # Node.js
import os                                  const url = process.env.DATABASE_URL;
url = os.environ["DATABASE_URL"]           const tok = process.env.DATABASE_AUTH_TOKEN;
token = os.environ["DATABASE_AUTH_TOKEN"]
python
# main.py — the libSQL client ships with the runtime
import os, libsql_client

def handler(request):
    db = libsql_client.create_client_sync(
        url=os.environ["DATABASE_URL"],
        auth_token=os.environ["DATABASE_AUTH_TOKEN"],
    )
    db.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
    db.execute("INSERT INTO notes (body) VALUES (?)", ["hello"])
    rows = db.execute("SELECT body FROM notes").rows
    db.close()
    return {"notes": [r[0] for r in rows]}

A database attached to a function cannot be deleted (409): detach it first. The storage tier is a hard cap; writes fail past it.

The database server cannot call your functions, so migrations are explicit. Deploy the migrator with the “migrate” kind — no service, no route, no hostname, so private by construction — designate it, then trigger it on demand from the UI, the API or an MCP agent.

Billing: the database instance costs the same per GB-second as a function of the same size (and nothing while it sleeps), plus the storage tier you hold, prorated per month. Raising the tier takes effect immediately; it cannot be lowered.

Backups and retention: generate a portable SQL export of your database at any time, downloadable for 24 h and restorable anywhere (gunzip -c export.sql.gz | sqlite3 mine.db). The service is in beta, so keep your own backups. If your balance stays at zero, the data is kept for 7 days and then PERMANENTLY DELETED; topping up before the deadline cancels it. Deleting a database also erases its data from object storage, but your exports are kept.

Dedicated page: how databases work, who can access them, backup and cost

Custom domains

Serve a function on a hostname you own: api.example.com instead of the platform address. The platform terminates TLS for it and obtains the certificate itself, so you never upload one.

Add the domain from the function's page and publish the two records it shows you:

  • a TXT under _zerolith-challenge.your-domain, holding a token that proves you control the zone;
  • a CNAME from your hostname to the function's own platform address; no new record from us, and the target follows the platform if that address ever moves.

Nothing is routed on an unproven claim. The platform re-checks periodically; once the TXT record is seen it provisions the route and orders the certificate, which is usually ready within a minute. Only then does the domain report that it is serving.

Once verified, the TXT record has done its job and can be removed, since it is read exactly once. The CNAME is what carries traffic and must stay.

Up to 10 domains per account. Wildcards are not supported: the certificate is obtained over HTTP-01, which cannot validate a `*` name. Hostnames the platform already serves cannot be claimed.

A claim that is never verified is released after 14 days, so a hostname you stop wanting does not stay reserved for ever.

Releasing a domain stops it serving immediately. Because re-adding it means ordering another certificate, the hostname cannot be claimed again for 60 minutes.

Sizes and limits

Sizes are resource presets (memory × CPU). The price follows the reserved memory and CPU, multiplied by execution time:

Execution timeout configurable from 1 to 600 s. Scale from 0 (scale-to-zero) up to your function's max scale; min ≥ 1 keeps warm instances, billed continuously.

Per-account quotas:

Deployed functions50
Code size per function768 KiB
Environment variables100
… of which secrets50
Single value / total budget4 KiB / 512 KiB
API request body2 MiB
Invocations per account (at the edge)600 / min

Throughput is controlled at the edge, before your function even wakes. Public functions called without a key get their own per-function counter, so a spike on a public webhook does not drain the rest of the account's budget.

Lifecycle & scaling

A function scales up and down on its own (Knative). While idle it runs at zero instances and costs nothing; the first request triggers a brief cold start, then subsequent calls are served warm.

Invocation lifecycle
1 instance0 instancestimeoutstable windowrequestresponsescale → 0

timeout = max duration of a single execution; stable window = idle time before scaling back to zero instances.

Every deployment (new code, config or environment variable change) creates a new immutable revision of the function. Traffic only switches to it once it is ready to serve, so an update causes no downtime.

Tunable from the function form: timeout bounds a single execution (1–600 s); the stable window is the idle time before scaling back to zero; min scale at 0 enables scale-to-zero (on demand), ≥ 1 keeps warm instances (billed continuously); max scale caps concurrency. Billing only runs during execution (reserved memory × time), plus invocations.

Observability & metrics

Every function exposes live metrics: requests per second, latency (p50 / p95 / p99), active instances, cold starts, CPU and memory. The app's Status page displays them, and the same API is open to your scripts:

curl
# live snapshot for all your functions (rolling window, default 5 min)
curl -H "Authorization: Bearer $JWT" \
     "https://zerolith.io/api/functions/metrics?window_seconds=3600"

# time series for one function — one point per step (feeds the Status graphs)
curl -H "Authorization: Bearer $JWT" \
     "https://zerolith.io/api/functions/<id>/series?window_seconds=3600&step_seconds=60"

Returned fields: rps, latency_p50_ms / p95 / p99, instances, cold_starts, cpu_millicores, memory_mb. Rolling window adjustable from 1 minute to 7 days (default 5 min). These metrics are measured live, nothing is stored.

On the billing side, the usage API returns the account totals (requests, GB-seconds, cost) and the detail of every billed window:

curl
# billed requests, GB-seconds and cost — CURRENT MONTH by default
curl -H "Authorization: Bearer $JWT" https://zerolith.io/api/usage/summary

# account lifetime, or any explicit range (since/until are half-open)
curl -H "Authorization: Bearer $JWT" "https://zerolith.io/api/usage/summary?period=all"
curl -H "Authorization: Bearer $JWT" \
     "https://zerolith.io/api/usage/summary?since=2026-06-01T00:00:00Z&until=2026-07-01T00:00:00Z"

# every billed window, filterable per function, paginated with limit/offset
curl -H "Authorization: Bearer $JWT" \
     "https://zerolith.io/api/usage/windows?function_id=<id>&limit=100&offset=100"

Usage windows are the billing source of truth: they are kept even after the function is deleted.

Cron triggers

Any function can be fired on a schedule with a cron expression (UTC), through the API:

curl
curl -X POST -H "Authorization: Bearer $JWT" \
     -d '{"cron": "*/15 * * * *"}' \
     https://zerolith.io/api/functions/<id>/schedules

Cron invocations go through the same credit gate as external calls: a dry account wakes nothing.

Agent driving (MCP)

zerolith.io is also an MCP server with OAuth 2.1: connect Claude or any MCP client to your account and deploy, edit and invoke your functions in natural language, with no key to copy.

Set up MCP »

The server exposes 41 tools, all scoped to your account. Read tools require the mcp:read scope; the others require mcp:write and a verified account:

ToolScopeDescription
whoamimcp:readAuthenticated account and credit balance.
get_catalogmcp:readLanguages, size presets, deploy defaults and pricing.
list_functionsmcp:readList your deployed functions.
get_functionmcp:readOne function: config, URL, attached env vars and current source code.
deploy_functionmcp:writeDeploy a new function.
update_functionmcp:writeUpdate a function (code and/or config).
delete_functionmcp:writeDelete a function.
invoke_functionmcp:writeInvoke a deployed function (credit-gated).
get_function_metricsmcp:readLive metrics (rps, latency, instances, CPU/memory).
get_function_logsmcp:readRecent stdout/stderr of one function, newest first; optional substring filter.
get_database_logsmcp:readRecent sqld engine log of one database; platform storage identifiers are redacted.
get_usage_summarymcp:readBalance and billed totals (requests, GB-seconds, cost).
list_env_varsmcp:readList the account's environment variables.
get_env_varmcp:readOne variable (value readable for config; never for a secret).
create_env_varmcp:writeCreate a variable (config or secret).
update_env_varmcp:writeReplace a variable's value.
delete_env_varmcp:writeDelete a variable (409 if still attached to a function).
list_custom_domainsmcp:readList your custom domains and the DNS records to publish.
add_custom_domainmcp:writeAttach a domain name to a function (returns the DNS records to create).
delete_custom_domainmcp:writeRelease a custom domain and remove its route.
create_presigned_urlmcp:writeCreate a URL that invokes one function until a deadline, with no API key.
list_presigned_urlsmcp:readList a function's presigned URLs (never the tokens themselves).
revoke_presigned_urlmcp:writeRevoke a presigned URL before its deadline.
list_databasesmcp:readList your private databases.
get_databasemcp:readOne database: tier, size, scaling mode and attached functions.
create_databasemcp:writeCreate a private database (the token is never returned).
delete_databasemcp:writeDelete a database (409 if still attached).
attach_databasemcp:writeAttach a database to a function (injects the variables).
detach_databasemcp:writeDetach a database from a function.
get_database_metricsmcp:readLive status: CPU, memory, instances and disk used vs the tier.
set_database_sizemcp:writeChange a database's CPU/memory preset.
set_database_tiermcp:writeRaise a database's storage tier (upgrade only — it cannot be lowered).
rotate_database_tokenmcp:writeRotate a database's access token: the old one is withdrawn and attached functions redeployed.
set_database_always_onmcp:writeKeep a database always on, or let it sleep.
set_database_stable_windowmcp:writeChange how long a database stays up while idle before sleeping.
export_databasemcp:writeStart a SQL dump of the database (one export per hour per account).
get_database_exportmcp:writeThe latest export's status, with its download link once it is ready.
list_database_exportsmcp:writeA database's export history; only the newest is still downloadable.
set_database_migratormcp:writeDesignate one of your functions as the schema migrator.
unset_database_migratormcp:writeRemove the migrator designation (the function stays attached).
run_migrationmcp:writeRun the designated migration function, on demand.

The whole sequence, from wiring the MCP server to calling the function. The last beat really calls a deployed function; nothing runs until you start the demo.

zerolith · mcpoffline

Security & isolation

Every account runs in its own Kubernetes space, walled off by a network policy that denies everything by default. Your functions, environment variables and database live there together and do not leave: another account's pods have no route to yours.

Calls are authenticated at the edge, before a container even starts: an invalid key, a suspended account or an empty balance are refused without ever waking your code, and therefore without billing you. Your secrets are read back by nobody, not even you: neither the API, the dashboard nor an AI agent returns their value. API keys are stored hashed and shown once, at creation.

Dedicated page: isolation, edge authentication, secrets, agents and data