A Rust-based Durable Execution service that compiles TypeScript to WebAssembly Components.
This is designed to be an educational projects for those looking to build similar systems. I'm slower at writing blog posts than I am at code, though, so this is getting released first.
In the meantime, please reach out if you have questions and let me know if you do something cool with this!
- TypeScript Functions: Write serverless functions in TypeScript
- WebAssembly Components: Compiled to WASM Components via ComponentizeJS
- Custom Host Functions: Built-in HTTP client, logging, sql, and execution context
- Type-Safe: WIT-based interfaces for host/guest communication
- Persistent Storage: SQLite database for metadata, filesystem for artifacts
- RESTful API: HTTP API for function management and execution
- CLI Interface: Complete command-line tool for all operations
TypeScript Source
↓ (esbuild)
JavaScript Bundle
↓ (componentize-js)
WASM Component
↓ (wasmtime)
Execution
- derp-cli: Command-line interface
- derp-server: HTTP API server (Axum)
- derp-runtime: WASM runtime with custom host functions (wasmtime)
- derp-build: Build pipeline (esbuild + componentize-js)
- derp-storage: Storage layer (SQLite + filesystem)
- derp-types: Shared types
- Rust 1.89.0+
- Node.js (for esbuild and componentize-js)
- SQLite
# Install esbuild
npm install -g esbuild
# Install componentize-js
npm install -g @bytecodealliance/componentize-jscargo build --release./target/release/derp serveThe server will listen on http://127.0.0.1:8888 by default.
./target/release/derp function upload hello-world examples/hello-world.ts./target/release/derp function build status hello-world 1./target/release/derp function run hello-world --input '{"name": "World"}'# Start HTTP server
derp serve [--addr ADDRESS]# Upload a function
derp function upload <NAME> <FILE>
# List functions
derp function list
# Run a function
derp function run <NAME> --input <JSON># Get build status for a function
derp function build status <NAME> <ID>
# List builds for a function
derp function build list <NAME>
# Get build status for a workflow
derp workflow build status <NAME> <ID>
# List builds for a workflow
derp workflow build list <NAME># Set an environment variable for a function
derp function env set <NAME> <KEY> <VALUE>
# Get an environment variable
derp function env get <NAME> <KEY>
# List environment variables
derp function env list <NAME>
# Delete an environment variable
derp function env delete <NAME> <KEY>
# Same commands work for workflows
derp workflow env set <NAME> <KEY> <VALUE>
derp workflow env list <NAME>POST /functions
Content-Type: application/json
{
"name": "my-function",
"source": "export function handler(req) { ... }"
}POST /functions/:name/execute
Content-Type: application/json
{
"body": { "key": "value" },
"metadata": {}
}GET /functions/:nameGET /functions/:name/builds/:id
GET /workflows/:name/builds/:idGET /functions/:name/env
PUT /functions/:name/env/:var_name
DELETE /functions/:name/env/:var_name
GET /workflows/:name/env
PUT /workflows/:name/env/:var_name
DELETE /workflows/:name/env/:var_nameEnvironment variables:
DATABASE_URL: Database path (default:sqlite:./data/db.sqlite)ARTIFACT_DIR: Artifact storage directory (default:./data/artifacts)WIT_DIR: WIT interface definitions (default:./wit)
Or use command-line flags:
derp --database-url sqlite:./my-db.sqlite \
--artifact-dir ./my-artifacts \
--wit-dir ./my-wit \
serveSee examples/README.md for detailed examples.
Every function must export a handler object with two optional methods:
export const handler = {
// Optional: One-time initialization (runs before first handle call)
setup() {
// Initialize database schema, set up state, etc.
},
// Required: Handle incoming requests
handle(req: Request<InputData>): Response {
// Process the request and return a response
}
};| Method | Required | Called | Purpose |
|---|---|---|---|
setup() |
No | Once, before first handle() |
Database schema creation, one-time initialization |
handle(req) |
Yes | Every invocation | Process requests and return responses |
┌─────────────────────────────────────────────────────────┐
│ Function Upload │
└─────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ TypeScript → WASM Component │
│ (esbuild + componentize-js) |
└───────────────────────────────┘
│
▼
┌────────────────────────┐
│ Function Ready │
└────────────────────────┘
│
┌──────────────────┴──────────────────┐
│ First Invocation │
▼ │
┌───────────────┐ │
│ setup() │ ◄─── Runs once │
│ (if exists) │ │
└───────────────┘ │
│ │
▼ │
┌───────────────┐ │
│ handle() │ ◄─── Runs on every request ─┘
└───────────────┘
│
▼
┌───────────────┐
│ Response │
└───────────────┘
import type { Request, Response } from 'derp:runtime/types';
import { success, error } from 'derp:runtime/context';
import { exec } from 'derp:runtime/sql';
import * as log from 'derp:runtime/logger';
interface InputData {
name: string;
}
export const handler = {
// Optional: runs once before first handle() call
setup() {
exec(`
CREATE TABLE IF NOT EXISTS greetings (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
)
`);
},
handle(req: Request<InputData>): Response {
log.info('Function called');
// req.body is already parsed - no JSON.parse needed!
const { name } = req.body;
// Store the greeting
exec('INSERT INTO greetings (name) VALUES (?)', name);
// success() accepts objects directly - no JSON.stringify needed!
return success({ greeting: `Hello, ${name}!` });
}
};HTTP Client:
import { fetch, getJson } from 'derp:runtime/http';
// Low-level fetch
const response = fetch({
method: 'GET',
url: 'https://api.example.com',
headers: [['User-Agent', 'DERP']],
});
// Convenience method for JSON APIs (auto-parses response)
const { status, body } = getJson<User>('https://api.example.com/users/1');Logger:
import * as log from 'derp:runtime/logger';
log.debug('Debug message');
log.info('Info message');
log.warn('Warning message');
log.error('Error message');Context:
import { getInfo, success, error } from 'derp:runtime/context';
const info = getInfo();
console.log(info.functionName, info.executionId);SQL (Per-Function SQLite Database):
import { exec, transaction } from 'derp:runtime/sql';
// Execute queries with parameter binding
const cursor = exec('SELECT * FROM users WHERE id = ?', 123);
// Cursor methods
cursor.toArray(); // All rows as array of objects
cursor.one(); // Exactly one row (throws if 0 or >1)
cursor.first(); // First row or undefined
// Transactions (auto-rollback on error)
const id = transaction(() => {
exec('INSERT INTO users (name) VALUES (?)', 'Alice');
return exec('SELECT last_insert_rowid() as id').one().id;
});Environment Variables:
import * as env from 'derp:runtime/env';
const apiKey = env.get('API_KEY'); // string | undefined
const debug = env.getOrDefault('DEBUG', 'false');name(PRIMARY KEY)status(storing | compiling | ready)content_hashversioncreated_atupdated_at
id(PRIMARY KEY)function_name(FOREIGN KEY)status(running | success | failure)input_fileoutput_fileerrorcreated_atupdated_at
id(PRIMARY KEY)function_name(FOREIGN KEY)namevaluecreated_atupdated_at
id(PRIMARY KEY)workflow_name(FOREIGN KEY)status(running | success | failure)input_fileoutput_fileerrorcreated_atupdated_at
id(PRIMARY KEY)workflow_name(FOREIGN KEY)namevaluecreated_atupdated_at
part1/
├── Cargo.toml # Workspace configuration
├── crates/
│ ├── derp-cli/ # CLI binary
│ ├── derp-server/ # HTTP API
│ ├── derp-runtime/ # WASM runtime
│ ├── derp-build/ # Build pipeline
│ ├── derp-storage/ # Storage layer
│ └── derp-types/ # Shared types
├── wit/ # WIT interface definitions
├── runtime/ # TypeScript type definitions
├── examples/ # Example functions
├── migrations/ # Database migrations
└── data/ # Runtime data (git-ignored)
├── db.sqlite # SQLite database
└── artifacts/ # Compiled functions
cargo testcargo fmtcargo build --releaseSee LICENSE file.