An editor, not a pipeline
Four tabs per function: script, package.json, triggers, secrets. Run executes what the server holds; Save and run appears only when your screen differs from it.
Self-hosted · Open source
Write your functions in the browser, then call them over HTTP or put them on a cron schedule. Deploys as a single Docker container: one Go binary, one SQLite file, nothing else to install.
How it works
Everything a function needs is on one screen — no local toolchain, no deploy step.
const { team } = await Bun.stdin.json();
const res = await fetch(`https://api.internal/teams/${team}`);
const members = await res.json();
console.log(JSON.stringify({ sent: members.length }));
Two ways to start a run, and a function can carry both at once.
POST /invoke/{name} with an API key. The request body becomes the function's stdin, and the response carries its result and duration.Every run records its trigger, status, duration, stdout, stderr, request payload and exit code. Including the runs that never happened: a schedule that came due while the server was down is logged as missed, with the count and the window.
Storage
SQLite. Functions, schedules, secrets, keys and logs all live in it. No database server to provision, and backing up the instance means copying one file.
replaced on every deploy
Litestream · optional
outlives the container
The disk underneath is not permanent. A container is replaced on every deploy and takes its filesystem with it. Litestream puts the file back before the server starts, so the container stays disposable and your data doesn't.
Or simply as a backup. Even on a server whose disk does survive, the same mirror is an off-site copy that is always current — no dump to schedule, no window during which the last hour is missing.
Leave it off and nothing else changes: FaaSBox runs from the local file alone, which is what you want on your own machine.
In the box
Four tabs per function: script, package.json, triggers, secrets. Run executes what the server holds; Save and run appears only when your screen differs from it.
An HTTP call and a cron tick take the same path: one Bun subprocess, the same environment, the same ceilings, the same log line. What differs is what happens when the box is busy — HTTP refuses with 429, a cron run waits its turn.
AES-256-GCM. Edit them as key/value pairs in the Environment tab, masked until you reveal them, and they land in the subprocess environment. Saving replaces the whole set — which is why you can see it.
Hashed on creation, revealed once, optionally restricted to named functions and given an expiry date. An expiry the server can't read is refused at creation rather than quietly dropped.
Trigger, status, duration, both streams, request payload, exit code, and a flag when something was truncated to fit. Retention is by row count, purged hourly.
Nothing runs unbounded: 30 seconds per run, 1 MB per request body, 1 MB captured per output stream, four concurrent runs. The sizes and the concurrency are defaults you change with an environment variable; the two timeouts are fixed.
By design
Every decision below takes a moving part out. What is left behaves the same way every time, and you can reason about all of it at once.
Get started
docker run -d -p 8080:8080 \
-e SUPERUSER_EMAIL=admin@example.com \
-e SUPERUSER_PASSWORD='…' \
-e FAASBOX_ENCRYPTION_KEY=$(openssl rand -hex 32) \
-v faasbox-data:/app/data/pb_data \
faasbox
localhost:8080/ — the editor. Sign in with the address and password from the command above; the account is created on first boot.
localhost:8080/_/ — the PocketBase admin underneath, if you ever want the raw collections.
Go 1.24+, Bun and Node.js, then bash infra/dev/dev.sh builds the editor and starts the server.