WASM Plugins
Building backend plugins with Rust and WebAssembly using the Orbis SDK
WASM Plugins
WASM (WebAssembly) plugins allow you to write backend logic in Rust that runs in a sandboxed environment. The Orbis SDK eliminates boilerplate and provides a powerful, ergonomic API.
Overview
WASM plugins can:
- Handle API route requests
- Execute complex business logic
- Process and transform data
- Interact with the database (with permissions)
- Make HTTP requests to external APIs
- Store and retrieve plugin state
- Emit events for other plugins
Prerequisites
bash
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM target
rustup target add wasm32-unknown-unknown Quick Start
1. Create a New Plugin
bash
mkdir my-plugin
cd my-plugin
cargo init --lib 2. Configure Cargo.toml
toml
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
orbis-plugin-api = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[profile.release]
opt-level = "s"
lto = true
strip = true 3. Write Your Plugin
rust
// src/lib.rs
use orbis_plugin_api::prelude::*;
use serde::{Deserialize, Serialize};
// Initialize the plugin with a single macro!
orbis_plugin! {
name: "my-plugin",
version: "0.1.0"
}
// Define your types
#[derive(Serialize)]
struct Greeting {
message: String,
timestamp: i64,
}
// Export handlers with the wrap_handler! macro
wrap_handler!(greet, greet_handler);
fn greet_handler(ctx: Context) -> Result<Response> {
let name = ctx.query_param("name").unwrap_or("World".to_string());
let greeting = Greeting {
message: format!("Hello, {}!", name),
timestamp: chrono::Utc::now().timestamp(),
};
Ok(Response::json(&greeting))
} That’s it! No memory management, no extern declarations, no raw pointers.
The Orbis SDK
The SDK provides a complete toolkit for plugin development:
Context - Request Information
rust
fn handler(ctx: Context) -> Result<Response> {
// Path parameters (e.g., /users/:id)
let user_id = ctx.param("id")?;
// Query parameters
let page: u32 = ctx.query_param_as("page").unwrap_or(1);
let limit = ctx.query_param_or("limit", "20".to_string());
// Headers
if let Some(auth) = ctx.header("Authorization") {
// Handle auth
}
// Parse JSON body
let request: MyRequest = ctx.body_as()?;
// Pagination helper
let pagination = ctx.pagination(100); // max limit
// Returns: Pagination { page, limit, offset }
// Check authentication
if ctx.require_auth().is_err() {
return Ok(Response::error(401, "Unauthorized"));
}
// Check admin status
if ctx.require_admin().is_err() {
return Ok(Response::error(403, "Forbidden"));
}
Ok(Response::ok())
} Response - Building Responses
rust
use orbis_plugin_api::prelude::*;
fn handler(ctx: Context) -> Result<Response> {
// JSON response (200 OK)
Ok(Response::json(&data))
// Custom status
Ok(Response::json_with_status(&data, 201))
// Convenience methods
Ok(Response::ok()) // 200 with {"success": true}
Ok(Response::created(&item)) // 201 with data
Ok(Response::no_content()) // 204
// Error responses
Ok(Response::error(400, "Bad request"))
Ok(Response::not_found("User not found"))
// With custom headers
Ok(Response::json(&data)
.with_header("X-Custom", "value")
.with_header("Cache-Control", "no-cache"))
// Paginated response
Ok(Response::paginated(items, pagination, total_count))
} State - Persistent Storage
rust
use orbis_plugin_api::sdk::state;
fn handler(ctx: Context) -> Result<Response> {
// Set a value (automatically JSON serialized)
state::set("counter", &42)?;
state::set("user", &user_data)?;
// Get a value (automatically deserialized)
let counter: Option<i32> = state::get("counter")?;
let user: Option<User> = state::get("user")?;
// Get with default
let count = state::get("counter")?.unwrap_or(0);
// Remove a value
state::remove("counter")?;
// Update with a function
state::update("counter", 0, |old: i32| old + 1)?;
// Convenience methods
state::increment("visits", 1)?;
state::decrement("remaining", 1)?;
state::push("history", &new_event)?;
// Scoped state (namespaced keys)
let user_state = state::scoped("user:123");
user_state.set("preferences", &prefs)?;
let prefs: Preferences = user_state.get("preferences")?.unwrap();
Ok(Response::ok())
} Database - Query and Execute
rust
use orbis_plugin_api::sdk::db;
fn handler(ctx: Context) -> Result<Response> {
// Query with automatic deserialization
let users: Vec<User> = db::query(
"SELECT * FROM users WHERE status = $1",
&["active"]
)?;
// Query single row
let user: Option<User> = db::query_one(
"SELECT * FROM users WHERE id = $1",
&[&id]
)?;
// Query scalar value
let count: i64 = db::query_scalar(
"SELECT COUNT(*) FROM users",
&[]
)?;
// Execute statements
let rows_affected = db::execute(
"UPDATE users SET last_login = NOW() WHERE id = $1",
&[&user_id]
)?;
// Transactions
let order = db::Transaction::new()
.execute(
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
&[&user_id, &total]
)
.execute(
"INSERT INTO order_items (order_id, product_id, qty) VALUES ($1, $2, $3)",
&[&order_id, &product_id, &qty]
)
.commit()?;
Ok(Response::json(&user))
} HTTP - External API Calls
rust
use orbis_plugin_api::sdk::http;
fn handler(ctx: Context) -> Result<Response> {
// Simple GET
let response = http::get("https://api.example.com/data")?.send()?;
let data: ApiResponse = response.json()?;
// POST with JSON body
let response = http::post("https://api.example.com/items")?
.json(&new_item)?
.send()?;
// With headers and bearer token
let response = http::get("https://api.example.com/protected")?
.bearer_token("my-token")
.header("X-Custom", "value")
.send()?;
// Check response status
if !response.is_success() {
let error: ErrorResponse = response.json()?;
return Ok(Response::error(response.status, &error.message));
}
// Error on non-2xx status
let response = http::get("https://api.example.com/data")?
.send()?
.error_for_status()?;
Ok(Response::json(&data))
} Logging
rust
use orbis_plugin_api::prelude::*;
fn handler(ctx: Context) -> Result<Response> {
log_info!("Processing request for user {}", user_id);
log_debug!("Request body: {:?}", body);
if let Err(e) = process() {
log_error!("Processing failed: {}", e);
return Ok(Response::error(500, "Internal error"));
}
log_trace!("Detailed trace info: {:#?}", details);
log_warn!("Deprecated endpoint used");
Ok(Response::ok())
} Error Handling
rust
use orbis_plugin_api::prelude::*;
fn handler(ctx: Context) -> Result<Response> {
// The ? operator works with SDK errors
let user: User = ctx.body_as()?;
let existing = db::query_one("SELECT * FROM users WHERE id = $1", &[&user.id])?;
// Create custom errors
if user.name.is_empty() {
return Err(Error::validation("Name is required"));
}
// Permission errors
if !has_access(&user) {
return Err(Error::permission_denied("Cannot access this resource"));
}
// Not found
let item = db::query_one("...", &[])?
.ok_or_else(|| Error::not_found("Item not found"))?;
// Errors automatically convert to appropriate HTTP responses
// Error::validation -> 400
// Error::permission_denied -> 403
// Error::not_found -> 404
// Error::internal -> 500
Ok(Response::json(&item))
} Plugin Manifest
Every plugin needs a manifest.json:
json
{
"name": "my-plugin",
"version": "1.0.0",
"description": "My awesome plugin",
"author": "Your Name",
"permissions": [
"database:read",
"database:write",
"network:http"
],
"routes": [
{ "path": "/api/greet", "method": "GET", "handler": "greet" },
{ "path": "/api/users", "method": "GET", "handler": "list_users" },
{ "path": "/api/users", "method": "POST", "handler": "create_user" },
{ "path": "/api/users/:id", "method": "GET", "handler": "get_user" }
],
"pages": [...],
"config": {
"api_key": "default-key",
"max_items": 100
},
"wasm_entry": "my_plugin.wasm"
} Available Permissions
| Permission | Description |
|---|---|
database:read | Read from database |
database:write | Write to database |
network:http | Make HTTP requests |
events:emit | Emit events |
state:read | Read plugin state |
state:write | Write plugin state |
Complete Example
rust
// src/lib.rs
use orbis_plugin_api::prelude::*;
use orbis_plugin_api::sdk::{db, state};
use serde::{Deserialize, Serialize};
// Initialize plugin
orbis_plugin! {
name: "todo-plugin",
version: "1.0.0"
}
// Data types
#[derive(Debug, Serialize, Deserialize)]
struct Todo {
id: i64,
title: String,
completed: bool,
user_id: String,
}
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
#[derive(Deserialize)]
struct UpdateTodo {
title: Option<String>,
completed: Option<bool>,
}
// Handlers
wrap_handler!(list_todos, list_todos_handler);
wrap_handler!(create_todo, create_todo_handler);
wrap_handler!(get_todo, get_todo_handler);
wrap_handler!(update_todo, update_todo_handler);
wrap_handler!(delete_todo, delete_todo_handler);
fn list_todos_handler(ctx: Context) -> Result<Response> {
ctx.require_auth()?;
let user_id = ctx.user_id().unwrap();
let pagination = ctx.pagination(100);
let todos: Vec<Todo> = db::query(
"SELECT * FROM todos WHERE user_id = $1 ORDER BY id LIMIT $2 OFFSET $3",
&[&user_id, &pagination.limit, &pagination.offset]
)?;
let total: i64 = db::query_scalar(
"SELECT COUNT(*) FROM todos WHERE user_id = $1",
&[&user_id]
)?;
Ok(Response::paginated(todos, pagination, total as u64))
}
fn create_todo_handler(ctx: Context) -> Result<Response> {
ctx.require_auth()?;
let user_id = ctx.user_id().unwrap();
let input: CreateTodo = ctx.body_as()?;
if input.title.is_empty() {
return Err(Error::validation("Title is required"));
}
let todo: Todo = db::query_one(
"INSERT INTO todos (title, completed, user_id) VALUES ($1, false, $2) RETURNING *",
&[&input.title, &user_id]
)?.unwrap();
// Track creation count
state::increment("todos_created", 1)?;
log_info!("Created todo {} for user {}", todo.id, user_id);
Ok(Response::created(&todo))
}
fn get_todo_handler(ctx: Context) -> Result<Response> {
ctx.require_auth()?;
let user_id = ctx.user_id().unwrap();
let id: i64 = ctx.param_as("id")?;
let todo: Option<Todo> = db::query_one(
"SELECT * FROM todos WHERE id = $1 AND user_id = $2",
&[&id, &user_id]
)?;
match todo {
Some(t) => Ok(Response::json(&t)),
None => Ok(Response::not_found("Todo not found")),
}
}
fn update_todo_handler(ctx: Context) -> Result<Response> {
ctx.require_auth()?;
let user_id = ctx.user_id().unwrap();
let id: i64 = ctx.param_as("id")?;
let input: UpdateTodo = ctx.body_as()?;
// Check ownership
let existing: Option<Todo> = db::query_one(
"SELECT * FROM todos WHERE id = $1",
&[&id]
)?;
let existing = existing.ok_or_else(|| Error::not_found("Todo not found"))?;
if existing.user_id != user_id {
return Err(Error::permission_denied("Not your todo"));
}
let title = input.title.unwrap_or(existing.title);
let completed = input.completed.unwrap_or(existing.completed);
let todo: Todo = db::query_one(
"UPDATE todos SET title = $1, completed = $2 WHERE id = $3 RETURNING *",
&[&title, &completed, &id]
)?.unwrap();
Ok(Response::json(&todo))
}
fn delete_todo_handler(ctx: Context) -> Result<Response> {
ctx.require_auth()?;
let user_id = ctx.user_id().unwrap();
let id: i64 = ctx.param_as("id")?;
let rows = db::execute(
"DELETE FROM todos WHERE id = $1 AND user_id = $2",
&[&id, &user_id]
)?;
if rows == 0 {
return Ok(Response::not_found("Todo not found"));
}
log_info!("Deleted todo {} for user {}", id, user_id);
Ok(Response::no_content())
} Building
bash
# Development build
cargo build --target wasm32-unknown-unknown
# Release build (optimized)
cargo build --target wasm32-unknown-unknown --release
# Output location
target/wasm32-unknown-unknown/release/my_plugin.wasm Build Script
bash
#!/bin/bash
set -e
echo "Building plugin..."
cargo build --target wasm32-unknown-unknown --release
# Copy WASM file
cp target/wasm32-unknown-unknown/release/my_plugin.wasm ./
# Optional: Embed manifest in WASM
cat manifest.json | python3 add_custom_section.py \
my_plugin.wasm \
-s manifest \
-o my_plugin.wasm
echo "Done! Output: my_plugin.wasm" Optimizing WASM Size
bash
# Install wasm-opt (from binaryen)
brew install binaryen # macOS
apt install binaryen # Linux
# Optimize
wasm-opt -Os -o optimized.wasm my_plugin.wasm Testing
rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation() {
let input = CreateTodo { title: "".to_string() };
// Test validation logic
assert!(input.title.is_empty());
}
} Run with:
bash
cargo test Next Steps
- Building Plugins - Advanced build configurations
- Testing Plugins - Integration testing strategies
- Best Practices - Guidelines and patterns
On This Page
- Overview
- Prerequisites
- Quick Start
- 1. Create a New Plugin
- 2. Configure Cargo.toml
- 3. Write Your Plugin
- The Orbis SDK
- Context - Request Information
- Response - Building Responses
- State - Persistent Storage
- Database - Query and Execute
- HTTP - External API Calls
- Logging
- Error Handling
- Plugin Manifest
- Available Permissions
- Complete Example
- Building
- Build Script
- Optimizing WASM Size
- Testing
- Next Steps