Self-hosted · Open source

A lightweight FaaS platform
for TypeScript. Zero dependencies.

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.

MIT licensed · Go · Bun · SQLite

The FaaSBox editor: the function list, the four tabs of a function, its code, and the runner showing a successful run The FaaSBox editor: the function list, the four tabs of a function, its code, and the runner showing a successful run

How it works

Three steps, and it runs.

  1. 1

    Write, install and test in the browser

    Everything a function needs is on one screen — no local toolchain, no deploy step.

    • 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 saving downloads and installs them for you, in the background. 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.
    daily-report
    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 }));
  2. 2

    Give it a trigger

    Two ways to start a run, and a function can carry both at once.

    • An HTTP call. POST /invoke/{name} with an API key. 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.
  3. 3

    Read the log

    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

The database is a single file.

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.

Container
data.db your whole instance

replaced on every deploy

Litestream · optional

every write, as it happens restored before start-up
S3 bucket
data.db the same file, kept in sync

outlives the container

Two reasons to turn it on

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

Everything you need, nothing you have to wire up.

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.

Two triggers, one engine

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.

Secrets encrypted at rest

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.

Keys you can scope

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.

Logs that answer questions

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.

Bounded by design

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

Small enough to hold in your head.

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.

  • One boundary to secure. A single superuser administers the instance. No permission matrix to audit, no role that quietly grants more than you meant.
  • Runs are atomic. A call returns a complete result or an error. Nothing to reassemble on the client, no half-written response to detect.
  • Functions stay independent. Each one is written, invoked and tested on its own. When something breaks there is no hidden call graph to unwind first.
  • Nothing fires twice behind your back. A schedule missed during downtime is reported, never replayed — the distinction matters when the function bills a customer or sends mail.
  • One moving part. No control plane, no registry, no broker, no VPC, no serverless.yml. The platform is the container you started, and that is the whole of it.

Get started

One container, one command.

docker
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

Then

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.

Working on it locally

Go 1.24+, Bun and Node.js, then bash infra/dev/dev.sh builds the editor and starts the server.