Getting started with Cruft

A ten-minute walkthrough from installing Cruft to a running web server with an embedded SQLite database, a sandboxed plugin compartment, and a supply-chain-sealed dependency tree. Every command and its output comes from a real session against cruft 0.0.10.

This walkthrough takes you from nothing to a running web server with a database, a sandboxed plugin, and a supply-chain-sealed dependency tree, about ten minutes. Every command and its output was captured from a real session against cruft 0.0.10.

1. Get Cruft

Cruft ships as a single binary. Two paths today:

  • npm package: cruft on the npm registry wraps prebuilt platform binaries (linux/darwin, x64/arm64, win32-x64): npm install -g cruft.
  • From source: clone the repository and cargo build --release --bin cruft -p cruft; the binary lands in target/release/cruft.

Check it:

$ cruft --version
cruft 0.0.10

2. Hello, world

$ cat hello.js
const who = process.argv[2] ?? "world";
console.log(`hello, ${who}`);

$ cruft hello.js cruft
hello, cruft

cruft <file> runs a file (cruft run <file> is the explicit form); flags after the file go to your program's process.argv, like node. No project setup, no config file.

3. TypeScript, no build step

Rename nothing, configure nothing: a .ts extension is enough:

$ cat tasks.ts
interface Task { title: string; done: boolean }
const tasks: Task[] = [{ title: "read the docs", done: true }];
console.log(tasks.filter(t => t.done).length, "done");

$ cruft tasks.ts
1 done

Types are erased Node---strip-types-style (no type checking, that stays in your editor), and because erased spans become spaces, runtime errors point at your actual .ts lines with no source maps. Details: TypeScript support.

Try the REPL too: just run cruft with no arguments: persistent state, _ for the last result, tab completion, multi-line continuation.

4. Install and use a package

$ cat package.json
{"name":"tut","version":"1.0.0","dependencies":{"is-odd":"^3.0.0"}}

$ cruft install
+ is-number@6.0.0
+ [email protected]
cruft install: 2 installed, 0 skipped

Cruft's built-in package manager resolves against the npm registry exactly as npm does, hard-links from a global content-addressed store (pnpm-style), and writes cruft-lock.json; commit that file; re-installs from it are exact and network-free.

$ cat odd.cjs
const isOdd = require("is-odd");
console.log("3 is odd:", isOdd(3));

$ cruft odd.cjs
3 is odd: true

CommonJS and ESM both work, mixed, like modern node. (Current boundaries: devDependencies and lifecycle scripts aren't processed yet; see the package manager.)

5. A web server

The native idiom is a fetch-shaped handler:

$ cat server.mjs
import { serve } from "cruft:serve";
serve({
  port: 8080,
  handler: (req) => {
    const url = new URL(req.url);
    if (url.pathname === "/") return "hello from cruft";
    return new Response(JSON.stringify({ path: url.pathname }),
                        { headers: { "content-type": "application/json" } });
  },
});
console.log("serving on http://127.0.0.1:8080");

$ cruft server.mjs &
serving on http://127.0.0.1:8080
$ curl -s http://127.0.0.1:8080/
hello from cruft
$ curl -s http://127.0.0.1:8080/api/x
{"path":"/api/x"}

Return a string, a { status, headers, body } object, or a Response; handlers may be async. node:http's (req, res) style works too if you're porting. Details: cruft:serve.

6. A database, embedded

No install, no daemon: SQLite is part of the runtime (an independent engine that writes the real sqlite3 file format):

$ cruft -e 'const { Database } = require("bun:sqlite");
const db = new Database("app.db");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
db.prepare("INSERT INTO users (name) VALUES (?)").run("ada");
console.log(db.query("SELECT * FROM users").all());'
[ { id: 1, name: 'ada' } ]

The app.db it just wrote opens in any SQLite tool. There's also an embedded PostgreSQL engine and a boundary-validating ORM over both, see the SQL stack and the ORM.

7. Run untrusted code in a compartment

A Compartment is a fresh, empty realm: code inside can reach only what you hand it.

$ cruft -e 'const c = new Compartment({
  globals: { shout: (s) => s.toUpperCase() },
  timeout_ms: 100,
});
console.log(c.evaluate("shout(\"sandboxed\")"));
console.log(c.evaluate("typeof process"));'
SANDBOXED
undefined

No process, no fetch, no filesystem: they are simply absent. Nothing is blocking them; they were never placed in the realm. The timeout_ms budget stops runaway loops and cannot be caught from inside. Use it for plugins, tenant code, or model-generated code. Details: Compartments and capabilities.

8. See what your dependencies actually do

$ cruft --audit hello.js
hello, world
# cruft audit log — 1 records
# format: <caller>\t<capability>\t<operation>\t<unix_micros>
file:///…/hello.js	stdio	write(stdout)	1784994699155160

Every I/O operation, attributed to the module that made it, even console.log is a recorded stdio write. Then flip on enforcement for your dependency tree:

$ cruft --sealed-deps app.mjs

Your code runs normally; everything under node_modules is denied I/O; a compromised transitive dependency can't read your files or phone home, because the authority was never in its hands. (Full --sealed mode exists too, gating network, filesystem, and directory enumeration and supporting a stdio grant; --sealed-deps is the simplest starting point.)

Where to next