Repository navigation
Expand file tree
/
Copy pathnote-wasm-plugins.html
More file actions
302 lines (282 loc) · 21.3 KB
/
Copy pathnote-wasm-plugins.html
File metadata and controls
302 lines (282 loc) · 21.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Extending MockForge with WASM Plugins</title>
<meta name="description" content="How MockForge's WASM plugin system lets you write custom response generators, data transformations, and protocol handlers in any language that compiles to WebAssembly — sandboxed and language-agnostic." />
<link rel="icon" href="/public/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/public/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/public/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/public/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/public/styles.css" />
<link rel="alternate" type="application/rss+xml" title="MockForge Engineering Notes" href="/rss.xml" />
<script>
(function () {
try {
var storedTheme = localStorage.getItem('theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (storedTheme === 'dark' || (!storedTheme && prefersDark)) {
document.documentElement.classList.add('dark');
}
} catch (error) {
console.warn('Theme initialization failed:', error);
}
})();
</script>
<style>
html, body { height: 100%; }
* { transition: background-color 0.2s ease, border-color 0.2s ease; }
html.dark { color-scheme: dark; }
html.dark body { background-color: #0f172a; color: #e2e8f0; }
html.dark .bg-white { background-color: #1e293b !important; }
html.dark .bg-surface-subtle,
html.dark .bg-stone-50,
html.dark section:not(.bg-white):not([style*="background"]) { background-color: #0f172a; }
html.dark .text-text-primary,
html.dark .text-slate-900,
html.dark .text-brand-ink,
html.dark .text-brand-dark { color: #f1f5f9 !important; }
html.dark .text-text-secondary,
html.dark .text-slate-700,
html.dark .text-slate-600,
html.dark .text-slate-500,
html.dark .text-slate-300 { color: #cbd5e1 !important; }
html.dark .text-orange-700,
html.dark .text-orange-200 { color: #fdba74 !important; }
html.dark header { background-color: #0f172a !important; border-bottom-color: rgba(255, 255, 255, 0.1) !important; }
html.dark header nav a { color: #e2e8f0 !important; }
html.dark header nav a:hover { color: #ffffff !important; }
html.dark .border-black\/5,
html.dark .border-black\/10,
html.dark .border-slate-200,
html.dark .border-slate-300 { border-color: rgba(255, 255, 255, 0.1) !important; }
html.dark .ring-black\/5 { --tw-ring-color: rgba(255, 255, 255, 0.1) !important; }
html.dark .shadow-soft { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); }
html.dark .shadow-note { box-shadow: 0 14px 40px rgba(0, 0, 0, 0.35); }
html.dark code { background-color: #1e293b; color: #e2e8f0; }
/* code inside a <pre> is its own block; don't paint the inline-code box behind it */
html.dark pre code { background-color: transparent; color: inherit; }
.reveal { opacity: 0; transform: translateY(20px); transition: opacity .6s cubic-bezier(.16,1,.3,1), transform .6s cubic-bezier(.16,1,.3,1); }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; transition: none; } }
</style>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Extending MockForge with WASM Plugins",
"description": "Custom mocks without forking the codebase — write response generators, data transformers, and protocol handlers in any WASM-compilable language, sandboxed and simple to wire up.",
"datePublished": "2026-05-21T00:00:00.000Z",
"url": "https://mockforge.dev/note-wasm-plugins.html",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://mockforge.dev/note-wasm-plugins.html"
},
"author": {
"@type": "Organization",
"name": "MockForge",
"url": "https://mockforge.dev/"
},
"publisher": {
"@type": "Organization",
"name": "MockForge",
"logo": {
"@type": "ImageObject",
"url": "https://mockforge.dev/public/logo-transparent.png"
}
}
}
</script>
<!-- social:begin -->
<link rel="canonical" href="https://mockforge.dev/note-wasm-plugins.html" />
<meta property="og:title" content="Extending MockForge with WASM Plugins" />
<meta property="og:description" content="Custom mocks without forking the codebase — write response generators, data transformers, and protocol handlers in any WASM-compilable language, sandboxed and simple to wire up." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://mockforge.dev/note-wasm-plugins.html" />
<meta property="og:image" content="https://mockforge.dev/public/og-image.png" />
<meta property="og:site_name" content="MockForge" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Extending MockForge with WASM Plugins" />
<meta name="twitter:description" content="Custom mocks without forking the codebase — write response generators, data transformers, and protocol handlers in any WASM-compilable language, sandboxed and simple to wire up." />
<meta name="twitter:image" content="https://mockforge.dev/public/og-image.png" />
<!-- social:end -->
</head>
<body class="bg-surface-subtle text-text-primary antialiased font-sans">
<header class="sticky top-0 z-40 backdrop-blur bg-white/90 dark:bg-slate-900 border-b border-black/5 dark:border-white/10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<a href="/" class="flex items-center gap-3">
<img src="/public/logo-transparent.png" alt="MockForge logo" class="h-8 w-8" />
<span class="font-semibold tracking-tight text-brand-dark dark:text-white">MockForge</span>
</a>
<nav class="hidden md:flex items-center gap-3 lg:gap-4">
<a href="https://docs.mockforge.dev" class="text-text-secondary dark:text-gray-300 hover:text-text-primary dark:hover:text-white text-sm lg:text-base">Docs</a>
<a href="/engineering-notes.html" class="text-text-secondary dark:text-gray-300 hover:text-text-primary dark:hover:text-white text-sm lg:text-base">Notes</a>
<a href="/#features" class="text-text-secondary dark:text-gray-300 hover:text-text-primary dark:hover:text-white text-sm lg:text-base">Features</a>
<a href="/pricing.html" class="text-text-secondary dark:text-gray-300 hover:text-text-primary dark:hover:text-white text-sm lg:text-base">Pricing</a>
<a href="https://github.com/SaaSy-Solutions/mockforge" class="text-text-secondary dark:text-gray-300 hover:text-text-primary dark:hover:text-white text-sm lg:text-base">GitHub</a>
<a href="https://app.mockforge.dev/login" class="text-text-secondary dark:text-gray-300 hover:text-text-primary dark:hover:text-white text-sm lg:text-base">Log In</a>
<a href="https://app.mockforge.dev/register" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 lg:px-3 lg:py-2 rounded-lg text-white hover:opacity-90 shadow-soft whitespace-nowrap text-sm" style="background-color: #D35400;">
<span>Get started free</span>
</a>
<button id="themeToggle" class="p-2 rounded-lg border border-black/10 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/10" aria-label="Toggle dark mode">
<svg id="sun" class="h-5 w-5 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v2m0 14v2m9-9h-2M5 12H3m15.364 6.364-1.414-1.414M7.05 7.05 5.636 5.636m12.728 0-1.414 1.414M7.05 16.95l-1.414 1.414"/><circle cx="12" cy="12" r="4"/></svg>
<svg id="moon" class="h-5 w-5 hidden dark:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"/></svg>
</button>
</nav>
<button id="menuBtn" type="button" class="md:hidden p-2 rounded-lg border border-black/10 dark:border-white/20" aria-label="Open menu" aria-controls="mobileMenu" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="h-6 w-6"><path d="M3 12h18M3 6h18M3 18h18"/></svg>
</button>
</div>
</div>
<div id="mobileMenu" class="md:hidden hidden border-t border-black/5 dark:border-white/10 bg-white dark:bg-brand-dark">
<div class="px-4 py-3 space-y-2">
<a href="https://docs.mockforge.dev" class="block text-text-primary dark:text-white">Docs</a>
<a href="/engineering-notes.html" class="block text-text-primary dark:text-white">Notes</a>
<a href="/#features" class="block text-text-primary dark:text-white">Features</a>
<a href="/pricing.html" class="block text-text-primary dark:text-white">Pricing</a>
<a href="https://github.com/SaaSy-Solutions/mockforge" class="block text-text-primary dark:text-white">GitHub</a>
<a href="https://app.mockforge.dev/login" class="block text-text-primary dark:text-white">Log In</a>
<a href="https://app.mockforge.dev/register" class="inline-flex items-center gap-2 px-3 py-2 rounded-lg text-white hover:opacity-90 shadow-soft" style="background-color: #D35400;">Get started free</a>
</div>
</div>
</header>
<main class="mx-auto max-w-3xl px-4 py-12 sm:px-6 lg:px-8">
<article class="rounded-2xl bg-white p-8 shadow-note ring-1 ring-black/5 sm:p-10">
<a href="/engineering-notes.html" class="text-sm font-medium text-brand-orange hover:opacity-80">← Back to Engineering Notes</a>
<p class="mt-6 text-xs uppercase tracking-[0.24em] text-slate-500">May 21, 2026</p>
<h1 class="mt-3 text-3xl font-semibold tracking-tight text-brand-ink sm:text-4xl">Extending MockForge with WASM Plugins</h1>
<p class="mt-6 text-lg leading-8 text-slate-700">Custom mocks without forking the codebase. Write a response generator in Rust, Go, or C, compile to WebAssembly, and MockForge calls it at request time — sandboxed, language-agnostic, and simple to wire up.</p>
<h2 class="mt-10 text-2xl font-semibold tracking-tight text-brand-dark">Why WASM, not a scripting language</h2>
<p class="mt-4 leading-8 text-slate-700">Three reasons the plugin system chose WebAssembly over embedding a scripting runtime:</p>
<p class="mt-4 leading-8 text-slate-700"><strong>Sandboxed by default.</strong> A WASM module cannot syscall, open files, or reach the network. It receives only the data the host explicitly passes it — request context, mock state, and plugin inputs. For a tool that teams deploy in CI and internal environments, this matters: a malicious or buggy plugin can't escape its sandbox.</p>
<p class="mt-4 leading-8 text-slate-700"><strong>Language-agnostic.</strong> Compile to WASM from Rust, Go, C, C++, Zig, AssemblyScript, or any language with a WASM target. Teams already writing their services in Rust can write plugins in the same language.</p>
<p class="mt-4 leading-8 text-slate-700"><strong>Predictable performance.</strong> WASM runtimes (wasmtime in MockForge's case) compile to native code with predictable overhead — typically microseconds to call a plugin function, no GC pauses.</p>
<h2 class="mt-10 text-2xl font-semibold tracking-tight text-brand-dark">How plugins work</h2>
<p class="mt-4 leading-8 text-slate-700">A plugin is a WASM module that exports a set of lifecycle functions. MockForge discovers plugins from a configured path, loads them at startup, and calls into them at defined extension points:</p>
<pre class="mt-4 overflow-x-auto rounded-xl bg-slate-900 p-5 text-sm leading-6 text-slate-100"><code>plugins:
paths:
- ./plugins/custom-generator.wasm
- ./plugins/db-lookup.wasm</code></pre>
<p class="mt-4 leading-8 text-slate-700">Each plugin exports a <code class="rounded bg-stone-100 px-1.5 py-0.5 font-mono text-sm">plugin_metadata</code> function that declares which extension points it hooks — response generation, request validation, data transformation, or custom protocol handling.</p>
<h2 class="mt-10 text-2xl font-semibold tracking-tight text-brand-dark">Writing a custom response generator</h2>
<p class="mt-4 leading-8 text-slate-700">A plugin that generates responses based on the request body doesn't need to parse JSON inside the WASM — MockForge passes the parsed request data as typed values. Here's a Rust plugin skeleton:</p>
<pre class="mt-4 overflow-x-auto rounded-xl bg-slate-900 p-5 text-sm leading-6 text-slate-100"><code>use mockforge_plugin_sdk::*;
#[no_mangle]
pub extern "C" fn plugin_metadata() -> Metadata {
Metadata {
name: "discount-calculator",
version: semver!("0.1.0"),
hooks: &[Hook::ResponseGeneration],
}
}
#[no_mangle]
pub extern "C" fn generate_response(ctx: &RequestContext) -> Response {
let tier = ctx.request_body.field("customer_tier").as_str();
let base_price = ctx.request_body.field("base_price").as_f64();
let discount = match tier {
"premium" => 0.20,
"business" => 0.10,
_ => 0.0,
};
Response::json(serde_json::json!({
"final_price": base_price * (1.0 - discount),
"discount_applied": discount > 0.0,
}))
}</code></pre>
<p class="mt-4 leading-8 text-slate-700">Compile with <code class="rounded bg-stone-100 px-1.5 py-0.5 font-mono text-sm">cargo build --target wasm32-wasip1 --release</code>, place the <code class="rounded bg-stone-100 px-1.5 py-0.5 font-mono text-sm">.wasm</code> in the plugins directory, and the generator is available on every request.</p>
<h2 class="mt-10 text-2xl font-semibold tracking-tight text-brand-dark">Practical use cases</h2>
<p class="mt-4 leading-8 text-slate-700"><strong>Database-derived responses.</strong> A plugin that connects to a real database (via the host's allowed-socket list, not from inside WASM) to return live reference data within an otherwise mocked environment. This lets you mock the service boundary while keeping the data layer real — useful for integration tests that need to validate against actual reference tables.</p>
<p class="mt-4 leading-8 text-slate-700"><strong>Protocol-specific transforms.</strong> MockForge's HTTP bridge converts gRPC calls to REST automatically. A plugin can add custom header mapping or content negotiation logic for endpoints where the default conversion doesn't match the real API.</p>
<p class="mt-4 leading-8 text-slate-700"><strong>Dynamic scenario selection.</strong> Instead of hardcoding test scenarios in the mock config, a plugin can select a scenario based on the authenticated user, the time of day, or a test-run parameter passed as a custom header. The same mock deployment serves different teams with different scenarios.</p>
<h2 class="mt-10 text-2xl font-semibold tracking-tight text-brand-dark">When to use a plugin vs config</h2>
<p class="mt-4 leading-8 text-slate-700">If your custom behavior is expressible as a template expansion or a conditional in the YAML config, do that. The config path is simpler, easier to debug, and doesn't require a separate build step. Use a plugin when:</p>
<ul class="mt-4 space-y-2 leading-8 text-slate-700 list-disc list-inside">
<li>The response depends on an external data source</li>
<li>The behavior requires conditional logic across multiple requests (session state)</li>
<li>You need to implement a protocol or wire format the built-in listeners don't cover</li>
</ul>
<h2 class="mt-10 text-2xl font-semibold tracking-tight text-brand-dark">Growing the system</h2>
<p class="mt-4 leading-8 text-slate-700">The plugin SDK is at v0.3 and the hook surface grows with each release. Current extension points cover response generation, request validation, and data transformation. Upcoming releases add plugin access to the mock's stored state and multi-request session tracking, which enables plugin-authored scenario flows.</p>
<div class="mt-8 flex flex-wrap gap-3 text-sm">
<a href="https://docs.mockforge.dev/user-guide/plugins.html" class="inline-flex items-center rounded-xl bg-brand-orange px-4 py-2 font-medium text-white hover:opacity-90 transition">Plugin SDK Docs</a>
<a href="https://github.com/SaaSy-Solutions/mockforge/tree/main/examples/plugins" class="inline-flex items-center rounded-xl border border-black/10 px-4 py-2 font-medium text-text-primary hover:bg-black/5 dark:hover:bg-white/10 transition">Plugin Examples</a>
<a href="/pricing.html" class="inline-flex items-center rounded-xl border border-black/10 px-4 py-2 font-medium text-text-primary hover:bg-black/5 dark:hover:bg-white/10 transition">Pricing</a>
</div>
</article>
</main>
<footer class="py-10 border-t border-black/5 dark:border-white/10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-3">
<img src="/public/logo-transparent.png" alt="MockForge logo" class="h-6 w-6" />
<span class="text-sm text-text-secondary">© <span id="year"></span> MockForge. MIT or Apache-2.0.</span>
</div>
<div class="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-sm">
<a href="https://docs.mockforge.dev" class="text-text-secondary hover:text-text-primary">Docs</a>
<a href="/engineering-notes.html" class="text-text-secondary hover:text-text-primary">Notes</a>
<a href="/pricing.html" class="text-text-secondary hover:text-text-primary">Pricing</a>
<a href="/compare-wiremock.html" class="text-text-secondary hover:text-text-primary">Compare: WireMock</a>
<a href="/compare-mockserver.html" class="text-text-secondary hover:text-text-primary">Compare: MockServer</a>
<a href="https://github.com/SaaSy-Solutions/mockforge" class="text-text-secondary hover:text-text-primary">GitHub</a>
<a href="https://www.linkedin.com/company/mockforge" class="text-text-secondary hover:text-text-primary" target="_blank" rel="noopener noreferrer">LinkedIn</a>
<a href="https://app.mockforge.dev/legal/privacy" class="text-text-secondary hover:text-text-primary">Privacy</a>
<a href="https://app.mockforge.dev/legal/terms" class="text-text-secondary hover:text-text-primary">Terms</a>
<a href="https://app.mockforge.dev/legal/dpa" class="text-text-secondary hover:text-text-primary">DPA</a>
<a href="/trust.html" class="text-text-secondary hover:text-text-primary">Trust</a>
<a href="/contact.html" class="text-text-secondary hover:text-text-primary">Contact</a>
</div>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function () {
var toggle = document.getElementById('themeToggle');
var html = document.documentElement;
var menuBtn = document.getElementById('menuBtn');
var mobileMenu = document.getElementById('mobileMenu');
var year = document.getElementById('year');
if (year) year.textContent = new Date().getFullYear();
if (toggle) {
toggle.addEventListener('click', function () {
html.classList.toggle('dark');
var isDark = html.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
}
if (menuBtn && mobileMenu) {
var setMenu = function (open) {
mobileMenu.classList.toggle('hidden', !open);
menuBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
menuBtn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
};
menuBtn.addEventListener('click', function () {
setMenu(mobileMenu.classList.contains('hidden'));
});
mobileMenu.querySelectorAll('a').forEach(function (link) {
link.addEventListener('click', function () {
setMenu(false);
});
});
}
// Scroll-reveal for any element with .reveal (IntersectionObserver, no scroll listeners).
var reveals = document.querySelectorAll('.reveal');
if (reveals.length && 'IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('in');
io.unobserve(entry.target);
}
});
}, { threshold: 0.12 });
reveals.forEach(function (el) { io.observe(el); });
} else {
reveals.forEach(function (el) { el.classList.add('in'); });
}
});
</script>
</body>
</html>