Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

combd in ten minutes

You’ll make an app with a page and a counter, run it on your machine, then run it on a server. The counter is a durable object, one per id, each with its own SQLite database. Each request to it is one transaction.

1. Install

You need Rust, from rustup.rs, with its WebAssembly target:

rustup target add wasm32-unknown-unknown

And combd. Once a release is out, take one for your machine from https://github.com/jtwebman/combd/releases: Linux x86_64 and arm64, or macOS arm64 and x86_64. Unpack it and put combd on your PATH. Or build it, with Rust 1.99 or newer:

cargo install --locked --git https://github.com/jtwebman/combd combd \
  --no-default-features --features aot-lite

combd help lists what it does.

2. Make an app, and run it

combd new hello
cd hello
combd dev

combd dev builds the app’s Rust for wasm32 into modules/, and serves it on port 8080. Open http://hello.localhost:8080 and click. Or:

curl -X POST http://hello.localhost:8080/counter/demo/incr   # {"count":1}
curl -X POST http://hello.localhost:8080/counter/demo/incr   # {"count":2}
curl http://hello.localhost:8080/counter/other/              # {"count":0}

Stop it with Ctrl-C and start it again. The counts are still there, in data/.

3. What you made

combd.toml says what runs and where requests go:

[app]
name = "hello"
domains = ["hello.localhost"]

[http.web]               # a stateless process: the page
module = "hello"
routes = ["/"]

[durable.counter]        # a durable object per :id, with its own SQLite
module = "hello"
routes = ["/counter/:id/*"]

src/lib.rs is one function. A request comes in, a response goes out:

fn handle(req: Request) -> Response {
    match (req.kind, req.method, req.route()) {
        ("api", "GET", "/") => Response::html(PAGE),
        ("do", "POST", "/incr") => {
            sql::run("CREATE TABLE IF NOT EXISTS counter (n INTEGER NOT NULL)", &[]).unwrap();
            sql::run("INSERT INTO counter (rowid, n) VALUES (1, 1) ON CONFLICT (rowid) DO UPDATE SET n = n + 1", &[]).unwrap();
            count()
        }
        ("do", "GET", "" | "/") => count(),
        _ => Response::text(404, "not found\n"),
    }
}

kind is api for the page and do for the counter. A counter’s route() is what comes after its id. Its SQL runs in its own database, counter/demo apart from counter/other. The request is one transaction. If handle panics, its writes are rolled back, and the next request finds the counter as it was. combd answers only once the writes are safe on disk.

Each counter is a cell, a small sandboxed process. combd pauses any cell that runs too long, so a busy one can’t starve the others. One that crashes starts again clean.

4. Change it

Add a reset to handle:

("do", "POST", "/reset") => {
    sql::run("DELETE FROM counter", &[]).unwrap();
    count()
}

Run combd dev again, then:

curl -X POST http://hello.localhost:8080/counter/demo/reset   # {"count":0}

combd-sdk also has alarms (alarm::set), the clock (now_ms), and calls to other processes (call, send). An alarm comes back as an ALARM request. See The Rust SDK.

Files that need no code, like images, styles, or a whole site, go in a static/ folder beside combd.toml. See Static files.

5. Run it on a server

A node serves every app in a folder, laid out as <org>/<app>/. On a Linux server with combd on it, and a domain pointing there:

mkdir -p /srv/combd/apps/me/hello
# from your machine: the manifest and the built module are all it needs
# (and static/, if it has one)
scp -r combd.toml modules server:/srv/combd/apps/me/hello/

Put your domain in combd.toml’s domains, then start it with your certificate:

COMBD_DATA=/srv/combd/data \
COMBD_TLS_CERT=/etc/letsencrypt/live/hello.example.com/fullchain.pem \
COMBD_TLS_KEY=/etc/letsencrypt/live/hello.example.com/privkey.pem \
COMBD_REDIRECT_PORT=80 \
combd serve-apps interp /srv/combd/apps 443

interp runs the modules interpreted. serve-apps takes vars and secrets from a combd.dev.toml beside the manifest, if there is one. See Apps and the manifest. Compiled modules ([modules.<name>] engine = "compiled") are built by builders, which work through a fleet. On one node, keep them interpreted. See Compiled modules and builders.

6. More than one machine

A fleet keeps objects in a bucket you own on S3 or R2, moves them to where their users are, and survives losing a node. Apps reach it with combd deploy, over a node’s admin API. See Deploying and Running a fleet. examples/arena/cloud/ sets up three nodes on DigitalOcean with Terraform, with builders for compiled modules and systemd units. examples/arena/README.md walks through it. plan.md has the design and how it’s tested, with a formal spec and a simulator that crashes everything.