Skip to content

Add your own functions

Need your guests to call something the platform doesn't provide — a payment processor, an email API, a signing routine, a proprietary database client? Add a custom bridge: a native function your app defines once and calls from any guest — TypeScript, Rust, or Go — as an ordinary typed import.

No lattice. No broker. No RPC dispatcher. No JSON middleware. The bridge contract is a WIT interface; rusm build generates all the glue. You write one file — host.rs, host.ts, or host.go — and the rest is produced for you.

Get started in 30 seconds

The fastest way in is a ready-made template. The mailer template wires a Resend email bridge end-to-end — contract, host impl, guest call, capability grant, all generated:

sh
rusm new myapp --template mailer              # TypeScript guest (default)
rusm new myapp --template mailer --lang rust  # Rust guest
rusm new myapp --template mailer --lang go    # Go guest
cd myapp && rusm build && rusm serve

The weather template ships a Rust host bridge — the zero-overhead path useful as a starting point for CPU-critical or tight-loop callers:

sh
rusm new myapp --template weather             # Rust host, TypeScript guest
rusm new myapp --template weather --lang rust # Rust host, Rust guest
rusm new myapp --template weather --lang go   # Rust host, Go guest

Adding a bridge to an existing project

rusm generate bridge <name> scaffolds bridges/<name>/bridge.wit and a starter host.ts (or host.rs / host.go with --lang) into your current project, adds the rusm.toml entry, and regenerates glue on the next rusm build.

How a bridge works

A bridge is a directory bridges/<name>/ with a contract (bridge.wit) and a host impl — Rust, TypeScript, or Go. rusm build generates everything else.

host.rshost.tshost.go
Runs asCompiled into the host binaryResident actor (bridge:<name>)Resident actor (bridge:<name>)
Call overhead~few hundred ns (native ABI)~1–10 µs (actor round-trip + JSON)~1–10 µs (actor round-trip + JSON)
Best forCPU-critical, tight-loop callersI/O-bound work, 3rd-party JS SDKsI/O-bound work, existing Go libraries

A Rust bridge compiles directly into the host binary — zero delegation, no extra process. A TypeScript or Go bridge runs as a supervised resident actor; the generated delegation shim dispatches calls over the actor wire. Both are capability-gated and default-deny: guests import the bridge only if their capability profile lists it.

1 — Define the contract

The WIT interface is the single source of truth for all three languages:

wit
// bridges/mailer/bridge.wit
package app:mailer@0.1.0;

interface smtp {
    record message {
        to:      string,
        subject: string,
        body:    string,
    }

    /// Send a transactional email. Returns true if the provider accepted it.
    send: func(msg: message) -> bool;
}

TypeScript host

Write one file — the runner, dispatch loop, and all Rust glue are generated:

ts
// bridges/mailer/host.ts — the ONLY file you write.
const API_KEY = process.env.RESEND_API_KEY ?? "";

export async function send(msg: { to: string; subject: string; body: string }): Promise<boolean> {
  const res = await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from:    "noreply@example.com",
      to:      msg.to,
      subject: msg.subject,
      html:    msg.body,
    }),
  });
  return res.ok;
}

The send function is async — the resident actor awaits the Resend response on its own Tokio task, so nothing blocks the calling guest.

Go host

go
// bridges/mailer/host.go — the ONLY file you write.
package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
)

var (
	apiKey = os.Getenv("RESEND_API_KEY")
	client = &http.Client{}
)

// Send delivers the email via Resend. The generated dispatcher calls this with the WIT
// `message` record already serialised into a json.RawMessage.
func Send(raw json.RawMessage) bool {
	var msg struct {
		To      string `json:"to"`
		Subject string `json:"subject"`
		Body    string `json:"body"`
	}
	if err := json.Unmarshal(raw, &msg); err != nil {
		return false
	}
	payload, _ := json.Marshal(map[string]string{
		"from":    "noreply@example.com",
		"to":      msg.To,
		"subject": msg.Subject,
		"html":    msg.Body,
	})
	req, _ := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return false
	}
	resp.Body.Close()
	return resp.StatusCode < 300
}

WIT records in Go bridges

record, enum, variant, and result params arrive as json.RawMessage — no intermediate allocation. Unmarshal into your own struct for full control over field naming. Primitive params (string, uint32, bool, …) are deserialised to the correct Go type directly.

Rust host

Rust bridges compile directly into the host binary — zero delegation, no actor, no JSON:

rust
// bridges/mailer/host.rs — the ONLY Rust an app must add for a Rust bridge.
// Add `reqwest = { version = "0.12", features = ["json"] }` to Cargo.toml.
use crate::bindings::app::mailer::smtp;
use rusm_wasm::wasmtime::component::HasSelf;
use rusm_wasm::{wasmtime, BridgeHost, BridgeLinker};

pub fn add_to_linker(linker: &mut BridgeLinker) -> wasmtime::Result<()> {
    smtp::add_to_linker::<_, HasSelf<BridgeHost>>(linker, |host| host)
}

impl smtp::Host for BridgeHost {
    async fn send(&mut self, msg: smtp::Message) -> bool {
        let Ok(api_key) = std::env::var("RESEND_API_KEY") else { return false };
        reqwest::Client::new()
            .post("https://api.resend.com/emails")
            .bearer_auth(&api_key)
            .json(&serde_json::json!({
                "from":    "noreply@example.com",
                "to":      msg.to,
                "subject": msg.subject,
                "html":    msg.body,
            }))
            .send()
            .await
            .map(|r| r.status().is_success())
            .unwrap_or(false)
    }
}

A Rust bridge reaches the calling process through BridgeHost's curated accessors: self.pid(), self.runtime(), self.caps() (gate yourself default-deny on a grant), and self.context() — the request's host-only claims context, which makes a bridge multi-tenant (act for client X vs Y) without the guest having any say. Nothing else of the host leaks in.

2 — Grant it (default-deny)

A bridge is reachable only by a component whose capability profile lists it:

toml
[capabilities.notifier]
inherits = "sandboxed"
bridges = ["mailer"]        # this profile may import the `mailer` bridge

[components.api]
capability = "notifier"

3 — Call it from a guest

A guest calls the bridge as a plain typed import — no dispatcher, no marshaling:

ts
/// <reference path="../../bridges.d.ts" />
import { http } from "rusm-ts";

// `mailer` is a typed global the per-app js-runner exposes (rusm build compiles it in).
// Bridge calls are synchronous from the guest's perspective — the fiber parks while
// the host resolves, then resumes. No await needed.
export default http({
  async post(req) {
    const { to, subject, body } = await req.json();
    const sent = mailer.send({ to, subject, body });
    return sent
      ? new Response("queued", { status: 202 })
      : new Response("delivery failed", { status: 502 });
  },
});
rust
use rusm_rs::http::{Params, Request, Response};
use serde::Deserialize;

#[derive(Deserialize)]
struct Payload { to: String, subject: String, body: String }

// The `bridge = …` attribute imports the bridge; call it at `crate::<iface>`.
#[rusm_rs::handlers(bridge = "app:mailer/smtp@0.1.0")]
pub mod api {
    use super::*;
    pub fn notify(req: Request, _p: Params) -> Response {
        let Ok(p) = req.json::<Payload>() else { return Response::bad_request() };
        if crate::smtp::send(&smtp::Message { to: p.to, subject: p.subject, body: p.body }) {
            Response::status(202)
        } else {
            Response::status(502)
        }
    }
}
go
import (
    "github.com/archan937/rusm/packages/rusm-go/web"
    smtp "go-api/internal/wit/app/mailer/smtp" // generated by rusm build
)

func run() {
    h := web.NewHandlers()
    h.Handle("notify", func(req web.Request, _ web.Params) web.Response {
        var p struct{ To, Subject, Body string }
        if err := req.JSON(&p); err != nil { return web.Status(400) }
        if smtp.Send(smtp.Message{To: p.To, Subject: p.Subject, Body: p.Body}) {
            return web.Status(202)
        }
        return web.Status(502)
    })
    h.Serve()
}

Rich types, not just strings

The bridge contract uses a record — and that's just the beginning. WIT carries the full value-type set: records, variants, enums, lists, options, results, tuples. Extend the mailer with an optional attachment:

wit
record attachment { filename: string, content: list<u8> }

send-with-attachment: func(msg: message, att: option<attachment>) -> bool;

Rust and Go receive native generated types (smtp::Message, smtp::Attachment); a TypeScript guest gets fully typed objects ({ filename: string; content: number[] }).

What rusm build generates

rusm build discovers bridges/, generates all host glue and per-guest bindings, vendors the contract into each granted component, and compiles a host binary that wires the bridges into the serve loop — same rusm serve command, bridges included.

  • Rust bridge — writes src/bindings.rs, src/bridges.rs, wit/, and the synthesized bindgen world. You author host.rs; everything else is generated.
  • TypeScript bridge — additionally writes src/bridge_<name>_delegate.rs (the Rust delegation shim), bridges/<name>/_runner.ts (the resident actor), and src/main.rs (if absent). Bun bundles the runner to wasm/bridge-<name>.js.
  • Go bridge — additionally writes the delegation shim, bridges/<name>/_runner.go, a minimal go.mod (if absent), and src/main.rs (if absent). TinyGo compiles the whole package to wasm/bridge-<name>.wasm.

All three runners register as supervised resident actors at startup — they stay alive for the node's lifetime and are restarted by the supervisor on failure.

Go deeper

MIT licensed