An editor, not a pipeline
Five tabs per function: script, package.json, triggers, secrets, and the files its folder holds on disk. 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
The code. Your function reads JSON on stdin and writes JSON on stdout. No framework, no handler signature to memorise, nothing to import.
package.json — optional. List your npm packages and save; the install runs on the server and you watch it from the tab — installing, then ready, or the tail of the failure if npm had something to say. There is nothing else to set up.
Run — one click executes the function and shows you its result, its output and its duration, without leaving the tab.
Files — the other tabs show what the database holds; this one shows what the machine has. The folder as it is on disk: your files, the lockfile, and the node_modules the install produced. Read-only, and the place to look when a version is not the one you expected.
Two ways to start a run, and a function can carry both at once.
An HTTP call. POST /invoke/{name} with an API key, created on the API keys page. The request body becomes the function's stdin, and the response carries its result and duration.
A cron schedule. Added in the Triggers tab: fifteen ready-made expressions, a free field, and a cap on concurrent runs. Changes take effect immediately — nothing restarts.
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.
Your dependencies come back on their own, either way. node_modules is not in the file — it is a build artifact, and a heavy one. What is stored is the lockfile, so a fresh container reinstalls exactly the versions the old one ran, before the first call arrives rather than during it.
In the box
Five tabs per function: script, package.json, triggers, secrets, and the files its folder holds on disk. 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 \
ghcr.io/antoineviau/faasbox:latest
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.