Writing Cells
JavaScript Cells
A JavaScript Cell is a module that exports a default object with a fetch handler.
Routing
Route on url.pathname:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/api/status") {
return Response.json({ ok: true, ts: Date.now() });
}
if (request.method === "POST" && url.pathname === "/echo") {
const body = await request.text();
return new Response(body, { headers: { "content-type": "text/plain" } });
}
return new Response("Not found", { status: 404 });
},
};
Reading request data
// URL and query params
const url = new URL(request.url);
const page = url.searchParams.get("page") ?? "0";
// JSON body
const data = await request.json();
// Form data
const form = await request.formData();
const name = form.get("name");
// Raw text
const text = await request.text();
// Headers
const auth = request.headers.get("authorization");
Outbound fetch
Cells have unrestricted outbound internet access via the standard fetch():
const res = await fetch("https://api.example.com/data");
const json = await res.json();
Fan out to multiple APIs in parallel:
const [weather, rates] = await Promise.all([
fetch("https://api.open-meteo.com/v1/forecast?...").then(r => r.json()),
fetch("https://api.exchangerate.host/latest").then(r => r.json()),
]);
Use AbortSignal.timeout() to enforce deadlines:
const res = await fetch("https://slow-api.example.com/data", {
signal: AbortSignal.timeout(5000), // 5 seconds
});
Returning responses
// Plain text
return new Response("hello", { headers: { "content-type": "text/plain" } });
// JSON
return Response.json({ ok: true, items: [] });
// HTML
return new Response("<h1>Hello</h1>", {
headers: { "content-type": "text/html; charset=utf-8" },
});
// Redirect
return Response.redirect("https://example.com", 302);
// Custom status and headers
return new Response("Created", {
status: 201,
headers: {
"content-type": "text/plain",
"x-request-id": crypto.randomUUID(),
},
});
TypeScript
ribo.toml supports a build command. Bundle TypeScript with any tool that produces a single ESM file:
[cell]
name = "my-cell"
js = "./dist/cell.js"
build = "npx esbuild src/cell.ts --bundle --outfile=dist/cell.js --format=esm --platform=browser"
ribo runs the build command before uploading the output file.
Rust / WASM Cells
Compile a Rust library to WebAssembly and deploy it as a Cell. The full HTTP handler — routing, logic, response generation — lives in the WASM binary.
How it works
When a request arrives, the tissue runtime:
- Serialises the request to a JSON string:
{ method, url, headers, body } - Calls your exported
fetch(req_json: String)function - Deserialises the returned
{ status, headers, body }into an HTTP response
Your Rust code never sees the JS runtime or the DOM — just plain Rust strings.
Cargo.toml
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.release]
opt-level = "z"
lto = true
src/lib.rs
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct IncomingRequest {
method: String,
url: String,
headers: std::collections::HashMap<String, String>,
body: Option<String>,
}
#[derive(Serialize)]
struct OutgoingResponse {
status: u16,
headers: std::collections::HashMap<String, String>,
body: String,
}
#[wasm_bindgen]
pub fn fetch(req_json: String) -> JsValue {
let req: IncomingRequest = serde_json::from_str(&req_json).unwrap();
let resp = match (req.method.as_str(), req.url.as_str()) {
("GET", _) => OutgoingResponse {
status: 200,
headers: [("content-type".into(), "text/plain".into())].into(),
body: format!("Hello from Rust! URL: {}", req.url),
},
_ => OutgoingResponse {
status: 405,
headers: [("content-type".into(), "application/json".into())].into(),
body: r#"{"error":"method not allowed"}"#.into(),
},
};
serde_wasm_bindgen::to_value(&resp).unwrap()
}
Build and deploy
# requires wasm-pack: https://rustwasm.github.io/wasm-pack/
wasm-pack build --target web --out-dir pkg
ribo.toml
[cell]
name = "my-rust-cell"
wasm = "./pkg/my_cell_bg.wasm"
build = "wasm-pack build --target web --out-dir pkg"
bindgen_glue = "./pkg/my_cell.js"
The build command runs automatically before upload. bindgen_glue points to the JS file generated by wasm-pack — it bridges the JS runtime and the WASM binary.
Bindings in cell code
When a Cell has bindings declared in ribo.toml, they appear on the env object passed to fetch:
export default {
async fetch(request, env) {
// env.DB — a c3 database
// env.BUCKET — a g7 object storage bucket
// env.FILES — a g7 static file tree
// env.API_KEY — a plain text value
},
};
See c3 API Reference and g7 API Reference for how to use these bindings.