Overview
WebAssembly (WASM) plugins offer a powerful alternative to native Go plugins, providing cross-platform compatibility and sandboxed execution. Unlike native.so plugins, WASM plugins:
- Run anywhere - Single
.wasmbinary works on any OS/architecture - No version matching - No need to match Go versions or dependency versions
- Sandboxed execution - WASM provides memory-safe, isolated execution
- Multi-language support - Write plugins in TypeScript, Go, Rust, or any WASM-compatible language
Plugin Interface
All WASM plugins must export these functions:| Export | Signature | Description |
|---|---|---|
malloc | (size: u32) -> u32 | Allocate memory for host to write data |
free | (ptr: u32) or (ptr: u32, size: u32) | Free allocated memory (Rust requires size for dealloc) |
get_name | () -> u64 | Returns packed ptr+len of plugin name |
init | (config_ptr, config_len: u32) -> i32 | Initialize with config (0 = success) |
http_pre_hook | (input_ptr, input_len: u32) -> u64 | HTTP transport pre-hook (request interception) |
http_post_hook | (input_ptr, input_len: u32) -> u64 | HTTP transport post-hook (non-streaming response interception) |
http_stream_chunk_hook | (input_ptr, input_len: u32) -> u64 | HTTP streaming chunk hook (per-chunk interception for streaming responses) |
pre_hook | (input_ptr, input_len: u32) -> u64 | Pre-request hook |
post_hook | (input_ptr, input_len: u32) -> u64 | Post-response hook |
cleanup | () -> i32 | Cleanup resources (0 = success) |
Return Value Format
Functions returning data use a packedu64 format:
- Upper 32 bits: pointer to data in WASM memory
- Lower 32 bits: length of data
Data Exchange
All complex data is exchanged as JSON strings. The host allocates memory usingmalloc, writes JSON data, and passes pointers to the plugin functions.
Getting Started
Choose your preferred language:- TypeScript
- Go (TinyGo)
- Rust
Hook Input/Output Structures
http_pre_hook
Header and Query Parameter Handling: Headers and query parameters in
request.headers and request.query preserve the original casing sent by the client. When looking up headers/query params, you should perform case-insensitive comparisons in your WASM plugin code to handle various casing (e.g., Content-Type, content-type, CONTENT-TYPE).For Go native plugins, use the built-in CaseInsensitiveHeaderLookup() and CaseInsensitiveQueryLookup() helper methods.http_post_hook
Called after the response is received from the LLM provider. Receives both the original request and the response. Input:The
http_post_hook is called in reverse order of http_pre_hook. Context values set in http_pre_hook are available in http_post_hook.http_stream_chunk_hook
Called for each chunk during streaming responses, BEFORE the chunk is written to the client. This hook allows plugins to modify or filter streaming chunks in real-time. Input:chunk field contains a BifrostStreamChunk struct serialized as JSON. It will contain the data from whichever response type is active:
- Chat completion streaming:
{"id":"...","object":"chat.completion.chunk","choices":[...],"model":"..."} - Text completion streaming:
{"id":"...","choices":[...]} - Responses API streaming:
{"type":"...","item":...} - Speech/Transcription/Image streaming: respective response fields
- Error:
{"error":{"type":"...","message":"..."}}
data: prefix or \n\n suffix).
Go Native vs WASM Plugins: In Go native plugins (
.so), you work directly with *schemas.BifrostStreamChunk typed structs. In WASM plugins, this struct is serialized to JSON for crossing the WASM boundary. The underlying data structure is the same.The
http_stream_chunk_hook is called in reverse order of http_pre_hook, same as other post-hooks.pre_hook
Input:post_hook
Input:Configuration
Configure your WASM plugin in Bifrost’sconfig.json:
Limitations vs Native Plugins
WASM plugins have some trade-offs compared to native Go plugins:| Aspect | Native (.so) | WASM |
|---|---|---|
| Performance | Fastest (in-process) | JSON serialization overhead |
| Cross-platform | Build per platform | Single binary everywhere |
| Version matching | Exact Go/package match required | No version requirements |
| Memory | Shared process memory | Linear memory (limited) |
| Languages | Go only | TypeScript, Go, Rust, etc. |
| Debugging | Full Go tooling | Limited debugging support |
| Security | Full process access | Sandboxed execution |
Source Code Reference
Complete hello-world examples are available in the Bifrost repository:- TypeScript: examples/plugins/hello-world-wasm-typescript
- Go (TinyGo): examples/plugins/hello-world-wasm-go
- Rust: examples/plugins/hello-world-wasm-rust
Troubleshooting
Module fails to load
Error:failed to instantiate WASM module
Solution: Ensure all required exports are present. Use a WASM inspection tool:
Memory allocation errors
Error:out of memory or invalid memory access
Solution:
- Increase heap size in your allocator
- Ensure you’re freeing memory after use
- Check for memory leaks in long-running plugins
JSON parsing errors
Error:failed to parse input JSON
Solution:
- Validate your JSON structures match expected schemas
- Handle optional/nullable fields properly
- Add error logging to identify malformed data
Build errors (TinyGo)
Error:package not supported by TinyGo
Solution: TinyGo doesn’t support all Go standard library packages. Avoid:
reflect(limited support)net/http(use raw JSON instead)- Complex generics
Build errors (Rust)
Error:cannot find -lc
Solution: For wasm32-unknown-unknown target, don’t link to libc. Ensure your Cargo.toml doesn’t require native dependencies.
Need Help?
- Discord Community: Join our Discord
- GitHub Issues: Report bugs or request features
- Documentation: Browse all docs

