Skip to main content
Everything else in a website’s source ships to the browser. A server function is a source file that never does: it runs in an isolated sandbox on Cactal’s servers with the website’s secrets available as env. Use one to send form leads to a CRM, call an API with a private key, or check something the browser can’t be trusted with.

Writing a function

A function is a server_function source file whose name, such as getSlots, starts with a letter and uses letters, numbers, -, and _:
  • on is the trigger: http or form. Each function has one.
  • env maps each secret’s name to its value, or undefined when it isn’t set.
  • Functions run on the WinterTC Minimum Common API (ECMA-429), the web platform Cloudflare Workers, Deno, Bun, and Node share: fetch to public addresses, Request, Response, Headers, FormData, Blob, File, streams, compression streams, URL, URLPattern, Web Crypto (PBKDF2 runs at most 100,000 iterations and derives at most 512 bits, and generated RSA keys are at most 2,048 bits), TextEncoder, TextDecoder, Intl, AbortController, MessageChannel, timers, and console. The Web Platform Tests for these APIs run against the sandbox on every change.
  • There is no DOM, file system, Node.js built-ins, WebAssembly, or WebSocket, and the build rejects code that uses an API the sandbox doesn’t provide.
  • A function can import @server, shared modules that use neither React nor @website, and pinned npm packages, which load their worker builds. SDKs built on fetch work, such as OpenAI, Anthropic, Stripe, Resend, Notion, HubSpot, Slack, and SendGrid. Packages that need Node.js networking, WebSocket, or WebAssembly don’t: use jose for JWTs, and call the service’s REST API with fetch instead. Pages can’t import function source.
  • Return values must be JSON-safe.

The http trigger

Page code imports an http function from @server/{name} and calls it. The types come from the handler, and the call never throws for expected failures:
The call is a same-origin POST /_framework/server/{name} with a JSON body. Draft hosts run the draft’s functions and the live site runs the published ones, with the same secrets. Error codes: Put expected outcomes, such as “this time slot is taken”, in data, not in errors. The payload’s type isn’t checked at runtime and anyone can call the function, so validate the payload in the handler, for example with zod’s safeParse. Cactal never retries a call.

The form trigger

A form function runs in the background after every form submission accepted as not spam, including draft submissions and submissions restored with Not spam. The visitor’s response never waits for it, and the submission is still stored and emailed. Use it, not an http function called from the form’s page, to send submissions to a CRM or another service: an http call skips spam screening. It receives the data the notification email carries:
A function handles each submission at most once. It gets 30 seconds and is never retried, so retry inside the function when an upstream is flaky.

Limits

Each call gets about 200 ms of CPU (time spent waiting on fetch is free), 10 seconds for http and 30 seconds for form, 64 MB of memory, 50 outgoing requests (4 at a time, each request body and response up to 5 MB), 64 KB of input, and 1 MB of output. Code outside the handler gets 500 ms when the function loads. Free organizations get 1,000,000 http calls a month, with warnings to owners at 80 percent and at the limit; paid plans have no monthly cap. See Plans and limits.

Secrets and redaction

Each secret’s value, plus its base64 (including inside a longer value such as a Basic auth header), base64url, hex, URL-encoded, form-encoded, and JSON-escaped forms, is replaced with [redacted:NAME] in function responses, console output, error messages, and run records. Values under 8 characters are left as written, since hiding them would also hide unrelated text. Outgoing requests are sent exactly as written. Code that deliberately disguises a secret, or sends it to a server it controls, can still leak it, as on any platform with environment variables.

Runs

Every call is recorded for 30 days with its input, output, console output, outgoing requests, and timings.
  • GET /v1/websites/{websiteId}/functions/runs lists runs newest first; filter with name.
  • GET /v1/websites/{websiteId}/functions/runs/{runId} returns one run.
  • POST /v1/websites/{websiteId}/functions/{name}/invoke runs an http function on the draft and returns its result. It makes the function’s real requests.
Runs and invoking need the websiteFunctions.view and websiteFunctions.invoke capabilities (full editors and above).