WebAssembly Functions

Makiatto supports running WebAssembly components as edge functions and file transformers, enabling you to add dynamic functionality to your static sites.

Overview

WASM support in Makiatto provides two types of components:

  1. HTTP Handlers - Edge functions that handle HTTP requests
  2. File Transformers - Middleware that transforms files before serving them

Both component types receive node context information, allowing you to write geo-aware and cluster-aware logic.

Runtime Capabilities

The WASM runtime uses WASI (WebAssembly System Interface) with the following capabilities:

  • Network access - HTTP requests and database connections to public endpoints (private IPs blocked for SSRF protection)
  • File system access - Read-only access to files within the domain directory
  • Environment variables - Access to configured env vars via WASI
  • Memory limits - Configurable per-function memory limits
  • Execution timeouts - Configurable per-function timeouts

File System Sandbox:

WASM functions have read-only access to their domain's directory, sandboxed to prevent accessing other domains or system files. The domain directory is mounted as / inside the WASM environment.

For example, if your domain is example.com with files in ./dist:

  • WASM sees / as the root
  • Opening /index.html reads ./dist/index.html
  • Opening /api/data.json reads ./dist/api/data.json
  • Cannot access files outside the domain directory or write files

This provides enough capability for edge computing use cases (reading templates, config files, making API calls) while maintaining security. For persistent data, use network calls to external databases or storage services.

Getting Started

First, fetch the WIT interface definitions:

maki wasm fetch

This will download WIT files to ./wit which define the interfaces your component must implement. Build your component using your language's WASM toolchain, for example:

  • Rust: wit-bindgen + wasm32-wasip2 target (Rust 1.82+) - Generates bindings and compiles to components
  • JavaScript: Javy - JavaScript to WebAssembly compiler
  • Go: TinyGo + wit-bindgen-go - Go compiler with WIT bindings
  • Python: componentize-py - Python bindings and compiler

Node Context

Every WASM function receives information about the node serving the request:

#![allow(unused)]
fn main() {
struct NodeContext {
    /// Node name
    name: String,
    /// Node geographic latitude
    latitude: f64,
    /// Node geographic longitude
    longitude: f64,
}
}

This enables use cases like:

  • Regional routing decisions
  • A/B testing by location
  • Custom load balancing logic
  • Debug information showing which node served the request

HTTP Handlers

HTTP handlers are WASM components that implement the http world from the Makiatto WIT interface.

Creating a Handler

The function signature is:

handle-request: func(ctx: node-context, req: request) -> response

Where you receive:

  • ctx - Node context with name, latitude, longitude
  • req - HTTP request with method, path, query, headers, and body

And return:

  • response - HTTP response with status code, headers, and body

Example implementation:

package main

import (
	"fmt"

	// Generated by: wit-bindgen-go generate --world http --out gen ./wit
	"gen/makiatto/http/handler"
)

func init() {
	handler.Exports.HandleRequest = handleRequest
}

func handleRequest(ctx handler.NodeContext, req handler.Request) handler.Response {
	body := fmt.Sprintf("Hello from node %s at (%f, %f)\nPath: %s",
		ctx.Name, ctx.Latitude, ctx.Longitude, req.Path)

	return handler.Response{
		Status: 200,
		Headers: []handler.Tuple2[string, string]{
			{"content-type", "text/plain"},
		},
		Body: handler.Some([]byte(body)),
	}
}

Deploying a Handler

Add the handler to your domain configuration:

# makiatto.toml
[[domain]]
name = "example.com"
path = "./dist"

[[domain.functions]]
# WASM file at ./dist/api/hello.wasm
path = "api/hello.wasm"

Sync to your cluster:

maki sync

Your function is now available at https://example.com/api/hello

The route is derived from the path - api/hello.wasm becomes /api/hello.

Configuration Options

Required fields:

  • path - Path to WASM file relative to domain directory (must stay within domain; route derived from this)

Optional fields:

  • methods - HTTP methods allowed (e.g., ["GET", "POST"]), defaults to all methods
  • env - Environment variables as key-value pairs (e.g., { API_KEY = "secret" })
  • env_file - Path to file containing environment variables
  • timeout_ms - Execution timeout in milliseconds
  • max_memory_mb - Maximum memory limit

File Transformers

File transformers are WASM components that process files before serving them. They implement the transform world.

Creating a Transformer

The function signature is:

transform: func(ctx: node-context, info: file-info, content: list<u8>) -> option<transform-result>

Where you receive:

  • ctx - Node context with name, latitude, longitude
  • info - File information (path, mime-type, size)
  • content - File content as bytes

And return:

  • option<transform-result> - Either the transformed content or null to pass through unchanged

Example implementation:

package main

import (
	"fmt"
	"strings"

	// Generated by: wit-bindgen-go generate --world transform --out gen ./wit
	"gen/makiatto/transform/transformer"

	"go.bytecodealliance.org/cm"
)

func init() {
	transformer.Exports.Transform = transform
}

func transform(ctx transformer.NodeContext, info transformer.FileInfo, content []byte) cm.Option[transformer.TransformResult] {
	if strings.Contains(info.MimeType, "html") {
		html := string(content)
		transformed := fmt.Sprintf("<!-- Served by %s at (%f, %f) -->\n%s",
			ctx.Name, ctx.Latitude, ctx.Longitude, html)

		return cm.Some(transformer.TransformResult{
			Content: []byte(transformed),
			MimeType: cm.Some(info.MimeType),
			Headers: []transformer.Tuple2[string, string]{
				{"x-transformed", "true"},
			},
		})
	}

	return cm.None[transformer.TransformResult]()
}

Deploying a Transformer

# makiatto.toml
[[domain]]
name = "example.com"
path = "./dist"

[[domain.transforms]]
# WASM file at ./dist/transform.wasm
path = "transform.wasm"
files = "**/*.html"

Configuration Options

Required fields:

  • path - Path to WASM file relative to domain directory (must stay within domain)
  • files - Glob pattern matching files to transform (e.g., **/*.html, **/*.{js,css})

Optional fields:

  • env - Environment variables as key-value pairs
  • env_file - Path to file containing environment variables
  • timeout_ms - Execution timeout in milliseconds
  • max_memory_mb - Maximum memory limit
  • max_file_size_kb - Skip files larger than this size

Sequential Transforms

Multiple transforms can be applied to the same file. They execute in the order they appear in your configuration:

[[domain.transforms]]
path = "minify.wasm"
files = "**/*.html"

[[domain.transforms]]
path = "inject-analytics.wasm"
files = "**/*.html"

Each transform receives the output of the previous transform.