Seed Hypermedia Plugin Architecture
A Comprehensive Design Document
Version: 1.0 Draft Date: January 2026 Status: Architecture Specification
Table of Contents
2.2 Figma Plugins
2.6 Obsidian Plugins
8.2 Network Access
8.3 Document Access
8.4 Storage
8.5 UI Capabilities
10.4 Fallback Hierarchy
12.1 State Update Events
12.3 Offline Behavior
15.4 Build Toolchain
16.3 Memory and Lifecycle
1. Executive Summary
This document specifies the plugin architecture for Seed Hypermedia, a decentralized collaboration platform. The architecture synthesizes lessons from eight major extensibility systems (VS Code, Figma, Factorio, Chrome, Minecraft, Obsidian, WordPress, and WebAssembly/Extism) to create a system that achieves:
Strong Security: True sandboxing via WebAssembly with capability-based permissions
Excellent User Experience: Declarative UI that feels native, with iframe escape hatches for advanced cases
Developer Ergonomics: TypeScript-first SDK with lower-level language support for power users
Data Portability: Schema-first block design enabling graceful degradation and search indexing
Platform Parity: Consistent behavior across Electron desktop app and web application
The core insight driving this architecture is that the historical tradeoff between security and flexibility can be broken by combining WebAssembly's sandboxing with a declarative UI layer. Plugins run in isolated Wasm runtimes within worker threads, communicating exclusively via message passing. UI is rendered by the host from declarative descriptions, with iframe-based WebViews available as a composable escape hatch for complex visualizations.
A key innovation is the Extension Point Model: rather than plugins defining entirely new block types, they extend native Seed blocks. A mermaid chart plugin doesn't create a "mermaid block" — it provides an extension point for the native code block when language=mermaid. This ensures fallback UI always exists and reduces fragmentation.
2. Prior Art Analysis
Before designing Seed's plugin architecture, we conducted extensive research into existing extensibility systems. Each represents different philosophies and tradeoffs that informed our decisions.
2.1 VS Code Extensions
Architecture Overview
VS Code employs a multi-process architecture built on Electron. Extensions run in a dedicated Extension Host process, separate from the main UI rendering process. This separation ensures that extension code cannot block the editor's interface — a misbehaving extension might hang the Extension Host, but users can still type, save, and close files.
The Extension Host is a Node.js process exposing the VS Code API. Extensions are JavaScript/TypeScript packages with a manifest (package.json) declaring:
Activation Events: When the extension should load (on command, on language, on file type)
Contribution Points: Static declarations for menus, commands, themes, keybindings
Extension Dependencies: Other extensions required
Key protocols enable language-agnostic tooling:
Language Server Protocol (LSP): Separates language intelligence (completion, diagnostics) from the editor
Debug Adapter Protocol (DAP): Standardizes debugger integration
Security Model
VS Code extensions are not sandboxed. The Extension Host has the same permissions as VS Code itself:
Full filesystem access (read, write, delete any file)
Unrestricted network requests
Ability to spawn child processes
Access to environment variables and system information
The official documentation acknowledges this explicitly: "providing a scalable solution with full Node.js support is the reason for separating the extension process from the sandboxed Renderer Process."
Security relies on external measures:
Marketplace malware scanning before publication
Publisher verification (domain ownership proof)
Extension signing
Community reporting
Research has demonstrated these measures are insufficient. Security researchers have successfully published malicious extensions that bypass all checks using simple evasion techniques (detecting sandbox environments, delaying malicious behavior).
Performance Characteristics
VS Code's performance model is sophisticated:
Lazy Loading: Extensions only activate when their activation events fire. An extension for Rust only loads when a .rs file opens.
Process Isolation: Extension crashes don't take down the editor
Contribution Point Separation: Static contributions (menus, themes) load without activating the extension
What We Learned
VS Code demonstrates that process isolation provides stability (crash protection, non-blocking UI) but not security. The lazy loading model via activation events is excellent and worth emulating. The LSP/DAP approach of standardized protocols for common functionality reduces redundant work.
What We Avoid
The complete lack of sandboxing is unacceptable for Seed. Users should not need to trust that extension authors are benevolent. The "marketplace scanning" security model provides an illusion of safety without substance.
2.2 Figma Plugins
Architecture Overview
Figma employs a sophisticated dual-environment architecture specifically designed for security. Each plugin consists of two components:
Main Thread Sandbox: Runs in QuickJS (a JavaScript engine compiled to WebAssembly). Has access to the Figma document API but no browser APIs.
iframe UI: Runs standard browser JavaScript with full browser APIs (fetch, DOM, localStorage) but no document access.
Communication between these environments uses postMessage, creating a clear security boundary.
The evolution of this architecture is instructive. Figma initially used the Realms shim for sandboxing — a JavaScript-based approach to creating isolated execution contexts. Within weeks of launch, security researchers discovered vulnerabilities allowing sandbox escape. Figma quickly pivoted to QuickJS compiled to WebAssembly, their backup plan.
QuickJS-to-Wasm is fundamentally more secure because:
Plugin code runs in a completely separate JavaScript VM
Object representations differ between sandbox and host (impossible to confuse them)
WebAssembly itself is sandboxed with no direct browser API access
All capabilities must be explicitly injected by the host
Security Model
Figma's model provides strong isolation:
The sandbox cannot access browser APIs (no fetch, no DOM, no storage)
The iframe cannot access the document (no reading designs, no modifications)
Network requests from the iframe cannot exfiltrate document data (the iframe never sees it)
The sandbox can only communicate outward through whitelisted APIs
Manual review focuses on user experience, not security auditing — the sandbox provides the security guarantee.
Performance Characteristics
Running JavaScript through an interpreter compiled to WebAssembly introduces overhead compared to JIT-compiled JavaScript. Figma accepts this tradeoff; they note the interpreter is slower than regular JavaScript since it's not a JIT, but performance is acceptable.
Key optimizations:
Main thread execution for direct canvas access (no IPC for document operations)
Lazy page loading (document pages load on demand)
Explicit lifecycle management (plugins must call figma.closePlugin())
UI Limitations
The iframe approach has UX drawbacks:
Styling discontinuity: Plugin UIs often look slightly "off" compared to Figma's native interface
Focus handling: Tab order across iframe boundaries is problematic
Drag and drop: Notoriously difficult across iframe boundaries
Theme inheritance: Plugins can't easily inherit Figma's design tokens
What We Learned
Figma proves that strong sandboxing is achievable for browser-based applications. The QuickJS-to-Wasm approach provides real security guarantees, not security theater. The dual-environment model (sandbox for logic, iframe for UI) cleanly separates concerns.
What We Improve
Figma's iframe-only UI leads to inconsistent plugin experiences. By offering a declarative UI layer that Seed renders natively, we can achieve better visual integration while keeping iframes as an escape hatch.
2.3 Factorio Modding System
Architecture Overview
Factorio uses a modified Lua 5.2 runtime for modding. The system operates in three distinct stages:
Settings Stage: Startup configuration (mod settings that users can adjust)
Prototype Stage: Defining game objects (recipes, machines, items, technologies)
Runtime Stage: Gameplay interaction through event handlers
A critical constraint drives the entire design: multiplayer determinism. Factorio uses lockstep networking where all clients simulate identical game state. Every mod must produce identical results across different machines, or multiplayer desyncs occur.
This constraint influences everything:
pairs() was modified to iterate in insertion order (standard Lua has arbitrary order)
math.random() uses a shared, seeded generator
Functions and metatables cannot be serialized (only data persists in saves)
The io and os modules are removed entirely
Security Model
Factorio's security model prioritizes determinism over isolation. Mods have significant game state access but operate within Lua's inherent limitations.
Removed standard library modules:
loadfile(), dofile() — no arbitrary file loading
coroutine — could introduce timing variations
io, os — filesystem and OS access
The require() function only loads files from mod directories. Mods cannot access system files or network resources.
Performance Characteristics
Lua was chosen for its lightweight embedding and fast execution. The event-driven architecture enables efficient interaction — mods register handlers for specific events (on_entity_died, on_player_crafted_item) rather than polling.
The prototype/runtime split means expensive operations (defining hundreds of recipes, loading graphics) happen once at load time, not during gameplay. Runtime code responds to events and manipulates existing objects.
What We Learned
Factorio demonstrates that a well-designed event system can provide significant flexibility even within a restricted environment. The staged loading model (settings → prototypes → runtime) cleanly separates concerns. The determinism constraints, while specific to games, illustrate how environmental requirements shape architecture.
What Doesn't Apply
Factorio's threat model differs from ours. They're protecting against accidental desyncs, not malicious code. Their users install mods knowing they're modifying their single-player or self-hosted game. Seed handles collaborative documents across organizations — the trust model is fundamentally different.
2.4 Chrome Browser Extensions
Architecture Overview
Chrome extensions use a multi-component architecture:
Service Workers: Background processing (replaced persistent background pages in Manifest V3)
Content Scripts: Inject into web pages with limited privileges
Popup/Options Pages: Extension UI
Sandboxed Pages: For eval() and dynamic code execution
Manifest V3 (the current extension format) introduced significant changes:
Service workers replaced background pages (ephemeral, event-driven)
Remote code execution banned (all code must be bundled)
declarativeNetRequest replaced blocking webRequest (affecting ad blockers)
Security Model
Chrome uses a permission-based security model declared in manifest.json:
Host permissions: Which URLs the extension can access
API permissions: Specific Chrome APIs (tabs, storage, bookmarks, etc.)
Content script permissions: Which pages to inject into
Manifest V3 tightened security:
Remote code banned — extensions cannot load JavaScript from external servers
eval() prohibited except in designated sandbox pages
Stricter Content Security Policy values
Content scripts have reduced privileges:
Can access page DOM but not the page's JavaScript context
Cannot directly call Chrome APIs — must message the service worker
Subject to the same-origin policy for network requests
Performance Characteristics
Service workers are ephemeral — they start on demand and shut down when idle. This reduces memory for inactive extensions but introduces cold start latency. Extensions must correctly handle the startup/shutdown lifecycle, persisting state to chrome.storage.
The declarativeNetRequest API moves network filtering to the browser process, improving performance over JavaScript-based webRequest but reducing flexibility (rules are static, not programmable).
Controversy
Manifest V3 has been controversial. The Electronic Frontier Foundation called it "deceitful and threatening," arguing it restricts privacy tools and ad blockers to benefit Google's advertising business. The shift toward declarative APIs (safer, more performant) comes at the cost of programmatic flexibility.
What We Learned
Chrome demonstrates the tension between security constraints and developer/user expectations. The permission model is more explicit than VS Code's but still relies on user approval rather than technical enforcement. The service worker model shows how ephemeral execution reduces resource usage.
What We Avoid
Chrome's permission prompts train users to click "Allow" without reading. Seed's capability model should be more granular and contextual, with meaningful distinctions that users can understand.
2.5 Minecraft Modding (Forge & Fabric)
Architecture Overview
Minecraft modding operates through mod loaders that intercept and modify the game's Java bytecode at runtime. Two dominant loaders exist:
Forge (established 2011):
Heavyweight, event-driven architecture
Significantly modifies vanilla Minecraft code
Comprehensive library of hooks and registries
Mature ecosystem with thousands of mods
Slower to update for new Minecraft versions
Fabric (established 2018):
Lightweight, minimalist design
Uses Mixin for bytecode injection
Base loader contains only essential hooks
Faster updates for new Minecraft versions
Growing ecosystem, particularly for performance mods
Forge works by patching Minecraft's code at load time, inserting event dispatchers throughout. Mods subscribe to events (BlockBreakEvent, EntitySpawnEvent) and register content through registries.
Fabric's Mixin framework allows direct bytecode injection. Developers specify injection points using annotations, and the framework weaves mod code into vanilla classes at runtime. This is more surgical but requires deeper understanding of Minecraft internals.
Security Model
Minecraft mods have no security boundary whatsoever. Mods are Java code running with full JVM permissions:
Complete filesystem access
Unrestricted network access
Ability to execute system commands
Full memory access within the JVM
Can modify any game code or other mods
The only protection is social: community trust, open-source code review, and distribution platform scanning. Malicious mods have caused real damage, including credential theft and cryptocurrency miners.
Performance Characteristics
Forge's heavyweight approach introduces baseline overhead but provides stability guarantees for complex modpacks with hundreds of mods. Its mature API reduces inter-mod conflicts.
Fabric's minimal footprint offers faster startup and lower memory overhead. Mixin's direct bytecode modification can be more performant than event dispatch for certain operations.
Both support large modpacks (100+ mods), though Forge historically handles complex dependencies better.
What We Learned
Minecraft demonstrates that users will accept significant security risks for powerful extensibility. The existence of both Forge (comprehensive but heavy) and Fabric (minimal but flexible) shows that different architectural philosophies serve different needs.
What We Avoid
The complete absence of sandboxing is unacceptable. Minecraft's trust model works only because users knowingly run local modifications to a game. Seed handles collaborative documents potentially containing sensitive information across organizational boundaries.
2.6 Obsidian Plugins
Architecture Overview
Obsidian is an Electron application, and its plugins are JavaScript/TypeScript code running directly in the application context. Plugins have access to:
The Obsidian API for note manipulation
The Node.js runtime (full filesystem, network, process spawning)
The DOM (can modify any UI element)
Electron APIs (native dialogs, system information)
The plugin architecture is straightforward: plugins are npm packages with a manifest.json, a main.js entry point, and optional styles.css. They load into the main Electron process with direct access to application internals.
Security Model
Obsidian's documentation is explicit: "Due to technical limitations in the plugin architecture, Obsidian cannot implement granular permission controls for community plugins. Instead, plugins inherit the full access level of the host application."
This means plugins can:
Read and modify any file on the user's filesystem
Make arbitrary network requests
Execute system commands
Access encryption keys (since Obsidian offers encrypted sync)
Potentially install persistent malware
The only mitigation is community review before plugins appear in the official directory. This catches obvious issues but not sophisticated attacks.
Performance Characteristics
Plugins run in the main Electron process, so poorly-written plugins can block the UI. There's no lazy loading — enabled plugins load at startup. Performance varies wildly by plugin quality.
What We Learned
Obsidian's architecture is essentially identical to VS Code's — both are Electron applications with unsandboxed JavaScript plugins. The honesty of Obsidian's documentation about security limitations is refreshing, but the limitations themselves are concerning.
What We Avoid
Seed requires real security, not trust-based security theater. Users collaborating on documents should not need to worry that a teammate's installed plugin might exfiltrate data.
2.7 WordPress Plugin Architecture
Architecture Overview
WordPress uses a hook-based plugin architecture implemented entirely in PHP. The system revolves around two types of hooks:
Actions: Execute code at specific points
add_action('wp_head', 'my_custom_function');
function my_custom_function() {
echo '<meta name="custom" content="value">';
}
Filters: Modify data before it's used
add_filter('the_content', 'modify_content');
function modify_content($content) {
return $content . '<p>Added by plugin</p>';
}
WordPress core (and themes) trigger hooks using do_action() and apply_filters(). A page request flows through hundreds of hooks, each providing an opportunity for plugins to inject behavior or modify content.
The WP_Hook class manages all registrations, storing callbacks with priorities that determine execution order.
Security Model
WordPress plugins have full server-side PHP execution capabilities:
Direct database access
Read/write any file the web server can access
Outbound network requests
Execute system commands (if not disabled by hosting)
Security relies on:
Code review for wordpress.org hosted plugins
Capability checks within plugin code
Nonce verification for form submissions
Prepared SQL statements
These are developer responsibilities, not enforced boundaries. Poorly-written plugins are a primary attack vector for WordPress sites, responsible for the majority of WordPress security breaches.
Performance Characteristics
Every active plugin adds overhead to every page request. Plugins registering many hooks or implementing expensive callbacks significantly impact performance. WordPress lacks lazy loading — all active plugins initialize on every request.
Caching (page, object, opcode) is the primary mitigation, but poorly-optimized plugins can defeat caching strategies.
What We Learned
WordPress's hook system is elegantly simple: register callbacks to named hooks, and they execute at the right time. The action/filter distinction (side effects vs. data transformation) is a useful conceptual model.
The WordPress ecosystem demonstrates that simple extensibility primitives can enable a massive plugin economy. The existence of plugins like WooCommerce shows that hook-based systems can support complex functionality.
What We Avoid
WordPress's complete lack of isolation makes it unsuitable as a security model. The server-side PHP execution model doesn't translate to our client-side/Wasm architecture anyway.
2.8 WebAssembly-Based Systems (Extism)
Architecture Overview
WebAssembly represents a new paradigm for plugin systems. Extism is a framework for building Wasm-based extensibility, providing:
Host SDKs: Embed Wasm runtime in applications (Rust, Go, Node.js, Python, Ruby, etc.)
Plugin Development Kits (PDKs): Write plugins in any language that compiles to Wasm
Standard interface: Common protocol for host-plugin communication
The architecture:
Host application embeds a Wasm runtime (Wasmtime, Wasmer, etc.)
Plugins compile to .wasm binaries from various languages
Host loads plugins into sandboxed Wasm instances
Communication occurs through well-defined interfaces (Wasm imports/exports)
WebAssembly Interface Types (WIT) define contracts between host and plugins with strong typing that goes beyond C's limitations.
Security Model
WebAssembly's security is fundamentally different from traditional plugin systems:
Capability-Based Security: By default, Wasm modules cannot access anything. The host explicitly grants capabilities:
Filesystem access (if any) is per-directory
Network access (if any) is per-domain
No implicit access to host memory, environment, or system calls
Sandboxed Execution:
Wasm memory is isolated from host memory
Call stack is protected (separate from data memory)
Plugins cannot access host code or other plugins' memory
Crashes are contained within the Wasm instance
Interface Enforcement:
Communication only through typed interfaces
No arbitrary memory access or function calls
Host controls exactly what the plugin can do
Performance Characteristics
WebAssembly approaches native performance through AOT or JIT compilation. Research shows Wasm is approximately 2.3× slower than native code for typical workloads, excluding cases where native code benefits from hardware-specific instructions.
Startup time is significantly faster than containers or VMs — Wasm instances spin up in milliseconds. Memory overhead per instance is minimal.
Real-World Adoption
WebAssembly plugins are used in production by:
Shopify: Merchant extensions in any language, sandboxed execution
Envoy/Istio: Proxy filters for service mesh
Goldman Sachs: API gateway customization
Apache HTTP Server: mod_wasm for request handling
Cloudflare Workers: Edge computing with Wasm isolates
What We Learned
WebAssembly solves the security-flexibility tradeoff that plagues other systems. True sandboxing with explicit capabilities, language-agnostic development, and near-native performance represent a genuine advance.
Extism specifically provides the infrastructure we need: host SDKs for both browser (JavaScript) and Electron (Node.js), PDKs for TypeScript and other languages, and a proven model for capability injection.
What We Build Upon
Seed's plugin architecture uses WebAssembly (via Extism) as its foundation. The sandboxing model, capability-based security, and host-mediated communication are exactly what we need. We extend this foundation with our declarative UI layer, schema-first blocks, and extension point model.
3. The Fundamental Tradeoffs
Plugin system design involves navigating a trilemma:
SECURITY
▲
/|\
/ | \
/ | \
/ | \
/ | \
/ | \
/ | \
/ | \
/________|________\
FLEXIBILITY PERFORMANCE
Security (isolation, sandboxing, capability enforcement)
Strong: Figma, WebAssembly
Weak: VS Code, Obsidian, Minecraft, WordPress
Flexibility (what plugins can do)
High: VS Code, Minecraft, WordPress
Constrained: Figma, Factorio
Performance (overhead, latency, resource usage)
Optimized: VS Code (lazy loading), Chrome (service workers)
Overhead: Figma (interpreter), sandboxed systems generally
Historical Constraints
Traditionally, you could pick two:
Security + Performance → Limited Flexibility Chrome's Manifest V3 moves toward declarative APIs that are secure and performant but restrict what extensions can do.
Flexibility + Performance → No Security VS Code and Minecraft provide maximum capability with minimal overhead but zero isolation.
Security + Flexibility → Performance Cost Figma's QuickJS-to-Wasm approach is secure and reasonably capable but incurs interpreter overhead.
Breaking the Tradeoff
WebAssembly changes this equation:
Security: Wasm's sandbox is baked into the virtual machine design, not a bolted-on afterthought
Performance: JIT/AOT compilation achieves near-native speed
Flexibility: Any language can compile to Wasm; host can expose rich capabilities
The remaining constraint is communication overhead. If plugins need to make thousands of fine-grained calls per frame, message passing latency accumulates. This is acceptable for Seed's use case (document editing, not real-time game simulation).
4. Design Requirements for Seed
Based on our analysis of existing systems and Seed's specific needs, we established these requirements:
Must Have
True Sandboxing: Plugins cannot escape their execution environment. No filesystem access, no unrestricted network, no access to other plugins' data without explicit permission.
Cross-Platform Parity: The same plugins must work in both Electron (desktop) and web browser deployments. Behavior should be identical.
Native-Feeling UI: Plugin UIs should be indistinguishable from Seed's native interface. No visual discontinuity, proper theming, correct accessibility.
Graceful Degradation: Documents with plugin-created content must remain usable without the plugin installed. No data loss, no broken rendering.
Schema-First Data: Plugin block data must be defined by schemas, enabling validation, indexing, and migration.
Collaboration Compatibility: Plugins must integrate correctly with Seed's real-time sync and CRDT-based collaboration.
TypeScript-First SDK: The primary development experience must be TypeScript with excellent types, IDE support, and documentation.
Should Have
Multi-Language Support: Developers who prefer Rust, Go, or other languages should be able to write plugins with more effort.
Extension Points over New Blocks: Plugins should enhance native blocks rather than creating parallel block types when possible.
Granular Permissions: Users should grant specific capabilities, not blanket "allow everything" approval.
Plugin-to-Plugin Communication: Plugins should be able to call each other's services through mediated channels.
Nice to Have
Hot Reload: Plugin developers should be able to iterate without restarting Seed.
Debuggability: Standard debugging tools should work with plugin code.
Analytics/Telemetry Hooks: Plugin authors should be able to understand usage patterns without building their own infrastructure.
5. Core Architecture
5.1 High-Level Overview
Seed's plugin architecture consists of three layers:
┌─────────────────────────────────────────────────────────────────────┐
│ SEED APPLICATION │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ MAIN THREAD │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌────────────────────┐ │ │
│ │ │ Seed UI │ │ Declarative │ │ iframe WebViews │ │ │
│ │ │ (React) │ │ Plugin UI │ │ (escape hatch) │ │ │
│ │ └─────────────┘ └─────────────┘ └────────────────────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────────┼────────────────────┘ │ │
│ │ │ │ │
│ │ Message Passing │ │
│ │ │ │ │
│ └───────────────────────────┼────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────┼────────────────────────────────────┐ │
│ │ WORKER THREAD(S) │ │
│ │ │ │ │
│ │ ┌───────────────────────▼───────────────────────────────┐ │ │
│ │ │ WASM RUNTIME (Extism) │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │
│ │ │ │ Plugin A │ │ Plugin B │ │ Plugin C │ ... │ │ │
│ │ │ │ .wasm │ │ .wasm │ │ .wasm │ │ │ │
│ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────────────┐ │ │ │
│ │ │ │ CAPABILITY LAYER │ │ │ │
│ │ │ │ Network │ Storage │ Document │ UI │ ... │ │ │ │
│ │ │ └────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ ELECTRON ONLY: NODE.JS BACKEND │ │
│ │ (optional, for native integrations) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
5.2 WebAssembly Runtime with Extism
We use Extism as our Wasm runtime framework. Extism provides:
Host SDK (runs in our worker):
Loads .wasm plugin binaries
Creates isolated Wasm instances
Injects host functions (capabilities) into the Wasm environment
Manages memory and handles data marshaling
Enforces resource limits (memory, execution time)
Plugin Development Kit (used by plugin authors):
Abstracts Wasm's low-level details
Provides idiomatic APIs for each supported language
Handles serialization/deserialization of complex types
Exposes host-provided capabilities
Why Extism over raw Wasm runtimes:
Multi-runtime support: Extism abstracts over Wasmtime, Wasmer, and browser Wasm
Host function protocol: Standardized way to inject capabilities
Memory management: Handles the complexity of passing data in/out of Wasm
HTTP handling: Built-in support for host-mediated network requests
Battle-tested: Used in production by Shopify, among others
5.3 Worker-Based Execution Model
Plugin Wasm code runs in Web Workers (browser) or Worker Threads (Node.js/Electron). This provides:
Non-Blocking UI: The main thread remains responsive regardless of plugin behavior. A plugin stuck in an infinite loop cannot freeze the editor.
Crash Isolation: A plugin crash terminates only its worker, not the entire application.
Security Boundary: Workers have limited access to the main thread's context. Combined with Wasm sandboxing, this creates defense in depth.
Architecture by Platform:
| Context | Web Browser | Electron | |---------|-------------|----------| | UI Extensions | Web Worker | Web Worker (renderer process) | | Services | Web Worker | Worker Thread (can use Node.js APIs) | | Document Access | Via main thread message | Via main thread message |
5.4 Message Passing Protocol
All communication between plugins and the host uses structured message passing:
// Host → Plugin
interface HostMessage {
type: 'invoke' | 'event' | 'response';
id: string; // Correlation ID for request/response
surface: string; // Which plugin surface (block, action, etc.)
payload: unknown; // Typed by surface schema
}
// Plugin → Host
interface PluginMessage {
type: 'request' | 'response' | 'ui-update';
id: string;
payload: unknown;
}
Message Types:
invoke: Host calls a plugin function (render block, execute action)
event: Host notifies plugin of something (state changed, user interaction)
response: Reply to a previous request
request: Plugin asks host to do something (fetch URL, write document)
ui-update: Plugin sends new declarative UI description
The protocol is symmetric and asynchronous. All operations that might take time return Promises resolved via response messages.
6. User Interface Architecture
6.1 The Declarative UI Foundation
Most plugin UIs don't need arbitrary HTML/CSS/JavaScript. They need forms, lists, buttons, and status displays. By providing a declarative component vocabulary, Seed can render plugin UIs natively, ensuring:
Visual Consistency: Plugin UIs inherit Seed's design system automatically
Theme Support: Dark mode, custom themes work without plugin author effort
Accessibility: Screen reader support, keyboard navigation built-in
Focus Management: Tab order flows correctly through the application
Performance: No iframe overhead for simple UIs
Core Component Vocabulary:
type SeedUIComponent =
// Layout
| { type: 'container'; direction: 'row' | 'column'; children: SeedUIComponent[] }
| { type: 'divider' }
| { type: 'spacer'; size: 'small' | 'medium' | 'large' }
// Typography
| { type: 'text'; content: string; variant?: 'body' | 'caption' | 'code' }
| { type: 'heading'; content: string; level: 1 | 2 | 3 }
// Form Controls
| { type: 'textInput'; id: string; label: string; placeholder?: string; value?: string }
| { type: 'textArea'; id: string; label: string; rows?: number; value?: string }
| { type: 'select'; id: string; label: string; options: Array<{value: string; label: string}>; value?: string }
| { type: 'checkbox'; id: string; label: string; checked?: boolean }
| { type: 'toggle'; id: string; label: string; enabled?: boolean }
| { type: 'slider'; id: string; label: string; min: number; max: number; value?: number }
// Actions
| { type: 'button'; id: string; label: string; variant?: 'primary' | 'secondary' | 'danger' }
| { type: 'buttonGroup'; children: Array<{ type: 'button'; id: string; label: string }> }
// Display
| { type: 'image'; src: string; alt: string; width?: number; height?: number }
| { type: 'icon'; name: string; size?: 'small' | 'medium' | 'large' }
| { type: 'badge'; content: string; variant?: 'info' | 'success' | 'warning' | 'error' }
| { type: 'progressBar'; value: number; max: number }
// Structure
| { type: 'list'; items: Array<{ id: string; primary: string; secondary?: string; icon?: string }> }
| { type: 'tabs'; id: string; tabs: Array<{ id: string; label: string; content: SeedUIComponent[] }> }
| { type: 'accordion'; sections: Array<{ id: string; title: string; content: SeedUIComponent[] }> }
// Escape Hatch
| { type: 'webView'; id: string; src: string; height: number };
This vocabulary covers ~95% of plugin UI needs. This approach follows successful prior art:
Raycast: Uses React-like components (<List>, <Form>, <Detail>) that render as native macOS UI. Developers literally cannot make an ugly plugin.
Slack Block Kit: Declarative JSON describes messages and modals. Slack renders them consistently.
VS Code Contribution Points: Static JSON declares menus, commands, settings — VS Code renders them natively.
Plugins describe their UI declaratively:
const ui: SeedUIComponent = {
type: 'container',
direction: 'column',
children: [
{ type: 'heading', content: 'Import Settings', level: 2 },
{ type: 'textInput', id: 'url', label: 'Source URL', placeholder: 'https://...' },
{ type: 'select', id: 'format', label: 'Format', options: [
{ value: 'json', label: 'JSON' },
{ value: 'csv', label: 'CSV' },
]},
{ type: 'checkbox', id: 'overwrite', label: 'Overwrite existing data' },
{ type: 'divider' },
{ type: 'buttonGroup', children: [
{ type: 'button', id: 'cancel', label: 'Cancel', variant: 'secondary' },
{ type: 'button', id: 'import', label: 'Import', variant: 'primary' },
]},
],
};
Seed renders this using its native React components. The plugin never touches the DOM.
6.2 The iframe Escape Hatch
Some plugins genuinely need capabilities beyond declarative UI:
Custom Visualizations: WebGL, Canvas, D3.js charts
Third-Party Embeds: YouTube players, map widgets
Rich Text Editing: CodeMirror, Monaco editor
Real-Time Collaboration: Whiteboard, collaborative drawing
For these cases, the webView component embeds an iframe:
const ui: SeedUIComponent = {
type: 'container',
direction: 'column',
children: [
{ type: 'heading', content: '3D Model Preview', level: 2 },
{ type: 'webView', id: 'preview', src: 'plugin://model-viewer/preview.html', height: 400 },
{ type: 'button', id: 'import', label: 'Import to Document', variant: 'primary' },
],
};
The iframe loads content bundled with the plugin. It has browser APIs (Canvas, WebGL, fetch) but runs in a null-origin sandbox.
6.3 Composing Declarative UI with iframes
The key insight — and what makes this architecture elegant — is that iframes are leaf nodes in the declarative tree, not a separate mode. This means:
The host controls overall layout, padding, chrome
Focus flows naturally (host manages entering/leaving the iframe)
Native components surround the iframe seamlessly
Plugin authors use one component (webView) instead of rebuilding everything
Example: Video Editor Plugin
const ui: SeedUIComponent = {
type: 'container',
direction: 'column',
children: [
{ type: 'heading', content: 'Video Trimmer', level: 2 },
// Declarative: native Seed components
{ type: 'textInput', id: 'title', label: 'Video Title', value: 'Untitled' },
// Escape hatch: custom video timeline UI
{ type: 'webView', id: 'timeline', src: 'plugin://video-editor/timeline.html', height: 200 },
// Declarative again
{ type: 'container', direction: 'row', children: [
{ type: 'text', content: 'Start: ' },
{ type: 'textInput', id: 'startTime', label: '', value: '00:00' },
{ type: 'text', content: 'End: ' },
{ type: 'textInput', id: 'endTime', label: '', value: '00:30' },
]},
{ type: 'button', id: 'apply', label: 'Apply Trim', variant: 'primary' },
],
};
The video timeline (a complex interactive canvas) uses the escape hatch, while everything else is native.
6.4 The Three-Context Model
With iframes in the picture, plugins span three execution contexts:
┌───────────────────────────────────────────────────────────────────────────┐
│ │
│ CONTEXT 1: Worker Thread │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Wasm Plugin Code │ │
│ │ - Business logic │ │
│ │ - Document manipulation │ │
│ │ - State management │ │
│ │ - Produces declarative UI descriptions │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ postMessage (structured data) │
│ ▼ │
│ CONTEXT 2: Main Thread │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Seed Host Application │ │
│ │ - Renders declarative UI using React │ │
│ │ - Creates iframe elements for webView components │ │
│ │ - Routes events back to Wasm │ │
│ │ - Mediates all communication │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ postMessage (to iframe) │
│ ▼ │
│ CONTEXT 3: iframe (when webView used) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Plugin UI Code (JavaScript) │ │
│ │ - Custom rendering (Canvas, WebGL, DOM) │ │
│ │ - Browser APIs (fetch, localStorage*) │ │
│ │ - Handles local interactions │ │
│ │ - Sends significant events back to main thread │ │
│ │ │ │
│ │ * localStorage scoped to plugin origin │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────┘
Communication Paths:
Wasm ↔ Main Thread: Structured messages through worker postMessage
Main Thread ↔ iframe: Messages through iframe postMessage
Wasm ↔ iframe: No direct path. All communication routes through main thread.
This architecture ensures the host can audit, rate-limit, and control all communication.
6.5 Unified SDK Authoring
Plugin authors shouldn't need to think about the three contexts. The TypeScript SDK provides a unified authoring experience:
// youtube-block.ts — single file, SDK handles the split
import { defineBlock, ui, webView } from '@seed/plugin-sdk';
export const youtubeBlock = defineBlock({
name: 'youtube-embed',
schema: z.object({
videoId: z.string(),
startTime: z.number().optional(),
}),
// Runs in Wasm context
async onRender(ctx) {
const { videoId, startTime } = ctx.blockData;
return ui.container({ direction: 'column' }, [
ui.heading('YouTube Video', 2),
// This webView is authored inline but extracted to iframe bundle
webView({
id: 'player',
height: 315,
// This function runs in iframe context
render: ({ onMessage, sendMessage }) => {
const container = document.createElement('div');
// Use YouTube iframe API
const player = new YT.Player(container, {
videoId: ctx.blockData.videoId, // Captured from outer scope
playerVars: { start: ctx.blockData.startTime || 0 },
events: {
onStateChange: (e) => sendMessage({ type: 'state', state: e.data }),
},
});
onMessage('seek', (time) => player.seekTo(time));
return container;
},
}),
ui.button('Copy Link', { id: 'copy', variant: 'secondary' }),
]);
},
// Runs in Wasm context
async onEvent(ctx, event) {
if (event.type === 'click' && event.id === 'copy') {
const url = `https://youtube.com/watch?v=${ctx.blockData.videoId}`;
await ctx.clipboard.write(url);
ctx.toast('Link copied!');
}
},
});
The SDK build toolchain:
Extracts webView.render functions into separate JavaScript bundles
Compiles the rest to Wasm
Sets up message passing plumbing automatically
Handles data serialization between contexts
7. Extension Surface Types
Seed plugins can register multiple surfaces — distinct ways of extending the application. Each surface type has specific capabilities and constraints.
7.1 Block UI Extensions
Block extensions render custom block types or provide alternative renderers for existing blocks.
Manifest Declaration:
{
"surfaces": {
"mermaidRenderer": {
"type": "block",
"extends": "code",
"when": { "language": "mermaid" },
"schema": "./schemas/mermaid.json",
"entry": "blocks/mermaid.wasm"
},
"customDiagram": {
"type": "block",
"blockType": "custom-diagram",
"schema": "./schemas/diagram.json",
"entry": "blocks/diagram.wasm"
}
}
}
Capabilities:
Read the block's data (validated against schema)
Return declarative UI (with optional webView)
Respond to user interactions
Request document writes (validated, persisted by host)
Lifecycle:
Block appears in viewport
│
▼
Load plugin Wasm (if not loaded)
│
▼
Call onRender(blockData) ──────► Returns UI description
│
▼
Host renders UI
│
├──► User interacts ──► onEvent(event) ──► May return new UI
│
├──► Block data changes (collab edit) ──► onRender(newData)
│
└──► Block leaves viewport ──► onUnmount() (cleanup)
7.2 Action Extensions
Actions are commands users can invoke. They appear in command palettes, context menus, or keyboard shortcuts.
Manifest Declaration:
{
"surfaces": {
"formatDocument": {
"type": "action",
"label": "Format Document",
"description": "Apply consistent formatting to the document",
"icon": "format-align-left",
"shortcut": "Cmd+Shift+F",
"parameters": {
"type": "object",
"properties": {
"style": { "enum": ["compact", "spaced", "academic"] }
}
},
"entry": "actions/format.wasm"
}
}
}
Capabilities:
Receive invocation with typed parameters
Read document content (with permission)
Write document changes (with permission)
Show progress UI
Return results
Execution Flow:
User triggers action (menu, shortcut, command palette)
│
▼
Parameter collection (if parameters defined)
│
▼
Call onExecute(params, context)
│
├──► Plugin reads document
│
├──► Plugin performs computation
│
├──► Plugin requests writes ──► Host validates & applies
│
└──► Plugin returns result ──► Host shows completion
7.3 Service Extensions
Services are long-running background processes that respond to events or provide capabilities to other plugins.
Manifest Declaration:
{
"surfaces": {
"aiService": {
"type": "service",
"provides": ["text-completion", "summarization"],
"capabilities": {
"network": ["api.openai.com"]
},
"entry": "services/ai.wasm"
}
}
}
Capabilities:
Run continuously (within worker lifecycle)
Respond to requests from other plugins (host-mediated)
Subscribe to document events
Make network requests (to permitted domains)
Use Cases:
AI/ML inference services
Real-time data sync with external systems
Background indexing or processing
Shared utilities for other plugins
7.4 UI Page Extensions
Page extensions add new pages/views to Seed's navigation, like settings panels or dashboards.
Manifest Declaration:
{
"surfaces": {
"analyticsPage": {
"type": "page",
"path": "/plugins/analytics",
"label": "Analytics Dashboard",
"icon": "chart-bar",
"navigation": "sidebar",
"entry": "pages/analytics.wasm"
}
}
}
Capabilities:
Full page rendering (declarative UI + webView)
Query parameters passed to plugin
Navigation integration (sidebar, tabs)
Deep linking support
8. The Capability Model
8.1 Capability-Based Security
Unlike permission-based models (where users approve capabilities at install time), capability-based security means the host explicitly provides each capability to plugins. Plugins cannot discover or access capabilities they haven't been granted.
In practice:
Plugin declares desired capabilities in manifest
User reviews and approves at install (or first use)
Host injects only approved capabilities into Wasm environment
Plugin cannot access unapproved capabilities — they don't exist in its environment
This is fundamentally more secure than permission prompts because:
Plugins can't try to access things they shouldn't
There's no API to even attempt forbidden operations
Security boundary is enforced by the Wasm VM, not application code
8.2 Network Access
By default, plugins cannot make network requests. The Wasm sandbox has no fetch, no XMLHttpRequest, no WebSocket.
Plugins that need network access:
Declare domains in manifest:
{
"capabilities": {
"network": ["api.example.com", "cdn.example.com"]
}
}
User approves network access (with domain list shown)
Host injects http_request function into Wasm:
// Inside Wasm, plugin calls:
const response = await Host.httpRequest({
method: 'GET',
url: 'https://api.example.com/data',
headers: { 'Authorization': 'Bearer ...' },
});
Host intercepts, validates, and performs the request:
Checks URL against approved domains
Can inject authentication headers
Can rate-limit requests
Logs all network activity
Why host-mediated networking:
Auditability: Host knows exactly what plugins communicate
Rate limiting: Prevents plugins from overwhelming APIs
Auth injection: Plugins don't need to store credentials
Domain enforcement: Can't be bypassed by clever URL construction
8.3 Document Access
Document access is the most sensitive capability for Seed. Plugins can request:
Read Access:
document:read:current-block — Only the block they're rendering
document:read:current-page — All blocks on current page
document:read:workspace — Entire workspace (sensitive!)
Write Access:
document:write:current-block — Modify their own block's data
document:write:current-page — Create/modify blocks on current page
document:write:workspace — Create/modify anything (very sensitive!)
Access Flow:
// Plugin requests document content
const page = await ctx.document.getCurrentPage();
// Host checks:
// 1. Does plugin have document:read:current-page capability?
// 2. Is user authenticated with access to this page?
// 3. Are there any collaboration locks?
// If approved, host returns sanitized document data
Write Validation:
All document writes go through the host:
// Plugin requests a write
await ctx.document.updateBlock(blockId, newData);
// Host validates:
// 1. Does plugin have write capability for this scope?
// 2. Does newData match the block's schema?
// 3. Is the write valid per CRDT rules?
// 4. Apply through sync layer (handles conflicts)
8.4 Storage
Plugins can store persistent data scoped to themselves:
// Simple key-value storage
await ctx.storage.set('preferences', { theme: 'dark' });
const prefs = await ctx.storage.get('preferences');
Constraints:
Storage is per-plugin, per-user (plugins can't read other plugins' storage)
Size limits enforced (e.g., 10MB per plugin)
Syncs with user's Seed account (available across devices)
Plugins cannot access browser localStorage, cookies, etc.
8.5 UI Capabilities
Plugins declare which UI surfaces they need:
{
"ui": {
"panel": true,
"modal": true,
"contextMenu": true,
"notification": true,
"webView": false
}
}
Why declare UI capabilities:
Host can optimize (no webView = lighter weight)
Security prompts can be more specific
Users understand what the plugin does
The webView: true capability triggers a specific warning (see section 14.3).
8.6 Permission Prompts and User Consent
When a plugin requests capabilities, Seed shows contextual prompts:
At Install Time (basic capabilities):
┌──────────────────────────────────────────────────────────┐
│ Install "Mermaid Diagrams"? │
│ │
│ This plugin will be able to: │
│ ✓ Render mermaid code blocks │
│ ✓ Store preferences │
│ │
│ [Cancel] [Install Plugin] │
└──────────────────────────────────────────────────────────┘
At First Use (sensitive capabilities):
┌──────────────────────────────────────────────────────────┐
│ "AI Assistant" wants to access: │
│ │
│ ⚠️ Read all content on this page │
│ ⚠️ Connect to api.openai.com │
│ │
│ This allows the plugin to send your document content │
│ to OpenAI's servers for processing. │
│ │
│ [Deny] [Allow Once] [Always Allow] │
└──────────────────────────────────────────────────────────┘
For Unrestricted UI (webView with network):
┌──────────────────────────────────────────────────────────┐
│ ⚠️ "YouTube Embed" requests enhanced access │
│ │
│ This plugin includes components with full browser │
│ access. It can: │
│ │
│ • Connect to any website │
│ • Use browser features (cookies, storage) │
│ • Track your activity within the plugin │
│ │
│ Only install if you trust the developer. │
│ │
│ [Cancel] [I Understand] │
└──────────────────────────────────────────────────────────┘
8.7 Capability Versioning
When a plugin updates and requests new capabilities, users must re-consent:
Version 1.0:
{ "capabilities": { "storage": true } }
Version 2.0:
{ "capabilities": { "storage": true, "network": ["api.example.com"] } }
On update, Seed shows:
┌──────────────────────────────────────────────────────────┐
│ "Example Plugin" update requires new permissions │
│ │
│ New capabilities requested: │
│ ⚠️ Connect to api.example.com │
│ │
│ [Skip Update] [Review & Update] │
└──────────────────────────────────────────────────────────┘
This prevents plugins from acquiring capabilities through silent updates.
9. Schema-First Block Design
9.1 Why Schemas Are Mandatory
Every block type (native or plugin-provided) must define a schema. This is not optional. Schemas enable:
Validation at Write Time: Malformed data never persists. If a plugin tries to write invalid data, the host rejects it.
Validation at Read Time: Plugins receive data guaranteed to match their schema. No defensive coding against malformed input.
Portability: Seed understands block structure without executing plugin code. This enables:
Rendering fallback UI for missing plugins
Search indexing of block content
Export to other formats
Data migration when schemas evolve
Type Safety: TypeScript SDK generates types from schemas. Plugin code has full IDE support.
9.2 Schema Definition with JSON Schema or Zod
Plugins can define schemas using JSON Schema or Zod (TypeScript):
JSON Schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"videoId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{11}$" },
"startTime": { "type": "number", "minimum": 0 },
"autoplay": { "type": "boolean", "default": false }
},
"required": ["videoId"]
}
Zod (TypeScript):
import { z } from 'zod';
export const youtubeBlockSchema = z.object({
videoId: z.string().regex(/^[A-Za-z0-9_-]{11}$/),
startTime: z.number().min(0).optional(),
autoplay: z.boolean().default(false),
});
export type YouTubeBlockData = z.infer<typeof youtubeBlockSchema>;
The SDK converts Zod schemas to JSON Schema for the manifest. Plugin code uses the Zod types directly.
9.3 Seed-Native Schema Primitives
Plugins can reference Seed's built-in types:
import { z } from 'zod';
import { SeedSchemas } from '@seed/plugin-sdk';
export const taskBlockSchema = z.object({
title: z.string(),
// Reference to another document
linkedDoc: SeedSchemas.documentRef,
// Rich text in Seed's native format
description: SeedSchemas.richText,
// Reference to a Seed user
assignee: SeedSchemas.userRef,
// Timestamp in Seed's format
dueDate: SeedSchemas.timestamp,
// Reference to an uploaded file
attachment: SeedSchemas.fileRef.optional(),
});
Why native primitives matter:
Resolution: Seed resolves refs correctly (permissions, sync, link previews)
Rich text: Plugin doesn't reinvent text formatting
Consistency: Users, dates, files work the same everywhere
Indexing: Seed can index relationships and text content
9.4 Schema Validation Flow
Plugin requests document write
│
▼
┌────────────────────────────┐
│ Host validates schema │
│ │
│ 1. Check required fields │
│ 2. Validate types │
│ 3. Run custom validators │
│ 4. Check constraints │
└────────────────────────────┘
│
├──► Invalid: Reject write, return error to plugin
│
└──► Valid: Apply through sync layer
│
▼
Persisted to network
│
▼
Replicated to collaborators
When loading blocks:
Block data retrieved from network
│
▼
┌────────────────────────────┐
│ Host validates schema │
│ (same checks) │
└────────────────────────────┘
│
├──► Invalid: Render with warning, offer repair
│
└──► Valid: Pass to plugin.onRender()
9.5 Schema Evolution and Migrations
Schemas evolve. Plugins must handle this gracefully.
Additive Changes (safe):
// v1
z.object({ videoId: z.string() });
// v2 - adds optional field
z.object({
videoId: z.string(),
startTime: z.number().optional(), // New, optional
});
Old data remains valid. Plugin handles missing startTime.
Breaking Changes (require migration):
// v1
z.object({ videoId: z.string() });
// v2 - renames field
z.object({ videoUrl: z.string() }); // Breaking!
Plugins declare migrations:
export const migrations = {
'1→2': (data: V1Data): V2Data => ({
videoUrl: `https://youtube.com/watch?v=${data.videoId}`,
}),
};
Seed runs migrations before passing data to the plugin:
Detect block's schema version
Load plugin's migration chain
Apply migrations sequentially
Pass migrated data to plugin
9.6 Graceful Degradation for Missing Plugins
When User A creates a block with Plugin X, and User B opens the document without Plugin X:
┌─────────────────────────────────────────────────────────┐
│ ⚠️ YouTube Embed │
│ │
│ This block requires the "YouTube" plugin to display. │
│ │
│ Block data: │
│ • videoId: "dQw4w9WgXcQ" │
│ • startTime: 42 │
│ │
│ [Install Plugin] [Show as JSON] │
└─────────────────────────────────────────────────────────┘
Because Seed has the schema, it can:
Show structured data (not raw JSON dump)
Offer to install the missing plugin
Allow editing raw data (with schema validation)
Preserve the block through edits (no data loss)
10. The Extension Point Model
10.1 Native Blocks as Foundation
Seed provides robust native blocks that handle common use cases without plugins:
Text: Paragraphs, headings, lists
Code: Syntax-highlighted code with language selection
Image: Uploaded or linked images
Video: Embedded video from URLs (handles YouTube, Vimeo, etc. natively)
Embed: Generic URL embeds (oEmbed)
Table: Data tables with sorting/filtering
File: File attachments
Divider: Visual separators
Callout: Highlighted information boxes
Critical design principle: Native blocks should handle as much as possible. For example, the native video block already handles YouTube URLs — it doesn't require a "YouTube plugin" to function. Plugins enhance the native experience (custom player controls, chapter markers, etc.) rather than providing basic functionality.
These native blocks work for all users without plugins. They're the foundation that ensures content is always accessible.
10.2 Extension Points for Native Blocks
Rather than creating "mermaid-block" or "youtube-block" as entirely new block types, plugins extend native blocks:
Code Block Extension:
{
"surfaces": {
"mermaidRenderer": {
"type": "block",
"extends": "code",
"when": { "language": "mermaid" },
"entry": "blocks/mermaid.wasm"
}
}
}
This says: "When a code block has language: mermaid, I can render it."
Video Block Extension:
{
"surfaces": {
"youtubeEnhanced": {
"type": "block",
"extends": "video",
"when": { "urlPattern": "youtube.com|youtu.be" },
"entry": "blocks/youtube.wasm"
}
}
}
This says: "When a video block's URL matches YouTube, I can provide enhanced rendering."
Benefits:
Fallback exists: Without the plugin, native code/video blocks render normally
Data portable: Block uses native schema (or extends it minimally)
No fragmentation: Users don't have 5 different "code block" types
Discoverable: Users see "Install plugin for enhanced rendering" rather than broken content
10.3 The "Open With" Paradigm
When multiple plugins can handle a block, Seed presents a choice similar to operating system file associations:
┌─────────────────────────────────────────────────────────┐
│ How would you like to render this code block? │
│ │
│ Language: mermaid │
│ │
│ ○ Seed (syntax highlighting only) │
│ ◉ Mermaid Diagrams (render as diagram) │
│ ○ Mermaid Pro (render with themes) │
│ │
│ ☐ Remember this choice for mermaid blocks │
│ │
│ [Cancel] [Use Selection] │
└─────────────────────────────────────────────────────────┘
User preferences stored:
{
"blockHandlers": {
"code:mermaid": "plugin:mermaid-diagrams",
"code:graphviz": "plugin:graphviz-renderer",
"video:youtube.com": "plugin:youtube-enhanced"
}
}
10.4 Fallback Hierarchy
When rendering a block, Seed follows this priority:
User's chosen plugin (if set)
Most capable installed plugin (by plugin-declared priority)
Native Seed renderer (always available)
If a plugin crashes or times out, Seed falls back to native rendering with a warning:
┌─────────────────────────────────────────────────────────┐
│ ⚠️ Plugin renderer failed │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ```mermaid │ │
│ │ graph TD │ │
│ │ A-->B │ │
│ │ ``` │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ [Retry] [Report Issue] [Use Seed Renderer] │
└─────────────────────────────────────────────────────────┘
11. Plugin-to-Plugin Communication
Plugins can expose services and call other plugins' services — but only through host mediation.
Exposing a Service:
// ai-service plugin
export const aiService = defineService({
name: 'ai-completion',
methods: {
complete: {
input: z.object({ prompt: z.string(), maxTokens: z.number() }),
output: z.object({ text: z.string() }),
async handler(input, ctx) {
const response = await ctx.network.fetch('https://api.openai.com/...', {
method: 'POST',
body: JSON.stringify({ prompt: input.prompt }),
});
return { text: response.choices[0].text };
},
},
},
});
Calling Another Plugin's Service:
// writing-assistant plugin
async function enhanceText(ctx: PluginContext, text: string) {
// Host mediates this call
const result = await ctx.plugins.call('ai-completion', 'complete', {
prompt: `Improve this text: ${text}`,
maxTokens: 500,
});
return result.text;
}
Host Mediation:
Plugin A calls ctx.plugins.call('service-b', 'method', data)
│
▼
┌────────────────────────────────────────────────────┐
│ Host validates: │
│ • Is Plugin A allowed to call Plugin B? │
│ • Does 'method' exist on service-b? │
│ • Does 'data' match method's input schema? │
│ • Rate limiting │
└────────────────────────────────────────────────────┘
│
├──► Denied: Error returned to Plugin A
│
└──► Approved: Route to Plugin B's Wasm instance
│
▼
Plugin B executes method
│
▼
Response validated against output schema
│
▼
Returned to Plugin A
Why Mediation:
Dependency tracking: Host knows which plugins depend on which
Failure isolation: Plugin B crashing doesn't take down Plugin A
Security: Plugin A can't access Plugin B's internals
Billing/quotas: Host can track API usage per plugin
12. Collaboration and Sync Integration
Seed is a real-time collaborative platform. Plugins must integrate correctly with the sync layer.
12.1 State Update Events
When block data changes (from any source — local edit, collaborator edit, sync resolution), the plugin receives an event:
export const diagramBlock = defineBlock({
// ...
async onStateUpdate(ctx, previousData, newData, source) {
// source: 'local' | 'remote' | 'migration'
if (source === 'remote') {
// Collaborator made a change
// Re-render with new data
return this.onRender(ctx);
}
// Local changes already reflected
return null; // No UI update needed
},
});
The host calls onStateUpdate whenever block data changes, regardless of source. Plugins don't need to poll or subscribe — the host pushes updates.
12.2 Transactional Document Writes
Plugins don't directly mutate document state. They request writes:
async function addItem(ctx: PluginContext, item: string) {
// Request a write
const result = await ctx.document.updateBlock(ctx.blockId, (data) => ({
...data,
items: [...data.items, item],
}));
if (result.conflict) {
// Another edit happened simultaneously
// result.resolved contains the merged state
// Plugin can accept or retry
}
}
Write Flow:
Plugin calls ctx.document.updateBlock() with a transform function
Host applies transform to current state
Host validates result against schema
Host submits to CRDT sync layer
Sync layer handles conflicts, ordering, persistence
Host notifies plugin of result (success, conflict, resolution)
12.3 Offline Behavior
Seed works offline. Plugins must handle:
Offline Writes:
const result = await ctx.document.updateBlock(id, transform);
if (result.status === 'queued') {
// Write accepted locally, will sync when online
// UI should reflect the local state
}
Network Requests Offline:
try {
const data = await ctx.network.fetch(url);
} catch (e) {
if (e.code === 'OFFLINE') {
// Show cached data or offline message
return ui.text('Content unavailable offline');
}
}
Sync Conflicts:
When the user comes back online, edits may conflict with remote changes. Seed's CRDT layer resolves most conflicts automatically. If manual resolution is needed, Seed handles the UI — plugins receive the resolved state via onStateUpdate.
13. Platform Considerations
13.1 Electron vs Web Parity
Seed runs as both an Electron desktop app and a web application. Plugins should work identically on both platforms.
Identical Behavior:
| Capability | Web | Electron | |------------|-----|----------| | Wasm execution | ✅ Web Worker | ✅ Web Worker (renderer) | | Declarative UI | ✅ | ✅ | | webView (iframe) | ✅ | ✅ | | Network (permitted) | ✅ | ✅ | | Storage | ✅ | ✅ (synced) | | Document access | ✅ | ✅ | | Plugin-to-plugin | ✅ | ✅ |
Electron-Only (explicitly unavailable on web):
| Capability | Web | Electron | |------------|-----|----------| | Filesystem access | ❌ | ❌ (intentionally disabled) | | System notifications | Limited | ❌ (intentionally disabled) | | Native menus | ❌ | ❌ (use Seed's UI) | | Shell integration | ❌ | ❌ (security risk) |
We intentionally disable Electron-specific capabilities to maintain parity. Plugins cannot rely on running in Electron.
13.2 Worker Spawning Strategy
For UI-centric surfaces (blocks, pages):
Workers spawn in the browser/renderer process
Direct message passing to main thread
UI updates are low-latency
For service surfaces:
On web: Web Worker in browser
On Electron: Web Worker in renderer (same as UI surfaces)
Not Node.js Worker Threads, to maintain parity
Why not use Node.js for Electron services:
Node.js Worker Threads have access to Node APIs (filesystem, native modules). Using them would break parity with web and create security inconsistencies. By keeping all plugin code in browser-like workers, we maintain uniform sandboxing.
13.3 Unsupported Capabilities
To maintain security and parity, these capabilities are explicitly not supported:
Filesystem access: Too dangerous, breaks web parity
Native system notifications: Inconsistent cross-platform, use Seed's notification system
Clipboard write without permission: Always requires user gesture
Background execution when app hidden: Web browsers throttle this; we don't fight it
Native dialogs: Use Seed's modal system
Window/process spawning: Security risk
Plugins that need these capabilities are outside Seed's plugin model. They could potentially be implemented as separate applications communicating via Seed's API.
14. Security Model
14.1 Wasm Sandbox Guarantees
WebAssembly provides strong isolation guarantees:
Memory Safety:
Wasm linear memory is separate from host memory
Plugins cannot read/write arbitrary host memory
Buffer overflows stay within Wasm's memory space
No pointer arithmetic into host space
Execution Isolation:
Wasm code cannot call arbitrary host functions
Only explicitly imported functions are available
Stack is protected (separate from linear memory)
No Implicit Capabilities:
No filesystem access unless injected
No network access unless injected
No DOM access (Wasm can't see the browser)
No environment variables, system info
Deterministic Execution:
Same inputs produce same outputs
No access to system time (unless injected)
No random numbers (unless injected)
Reproducible behavior
14.2 iframe Trust Boundaries
iframes (webView components) have different trust characteristics:
iframe CAN:
Access browser APIs (DOM, Canvas, WebGL)
Make fetch requests (subject to CORS)
Use localStorage (scoped to plugin origin)
Run arbitrary JavaScript
iframe CANNOT:
Access Seed's DOM (null origin, cross-origin isolation)
Access document data (never passed to iframe directly)
Read other plugins' storage
Escape the iframe boundary
Residual Risk:
If Wasm sends sensitive document data to an iframe for visualization, the iframe JavaScript could exfiltrate it via fetch. Mitigations:
CSP restrictions: Limit iframe's fetch destinations
Data minimization: Send only necessary data to iframe
User warnings: Clearly indicate plugins with iframe access
14.3 The "Unrestricted" UI Tier
Plugins declare their rendering mode:
{
"surfaces": {
"basicBlock": {
"type": "block",
"render": "sandboxed" // Declarative UI only
},
"complexBlock": {
"type": "block",
"render": "unrestricted" // Can use webView with network
}
}
}
"sandboxed" render mode:
Declarative UI components only
webView prohibited
Maximum security, minimal warnings
"unrestricted" render mode:
Can use webView component
iframe has browser APIs
Triggers security warning at install
The prompt for unrestricted UI clearly communicates risks:
⚠️ This plugin requests enhanced UI access
It can:
• Display custom web content
• Connect to external websites
• Use cookies and browser storage
Your document content may be visible to this plugin's
UI components. Only install if you trust the developer.
14.4 Attack Surface Analysis
Threat: Malicious plugin exfiltrates document data
Mitigations:
Wasm sandbox: Plugin can't access network without permission
Network whitelisting: Can only contact declared domains
User consent: User approves network access
Audit log: Host logs all network requests
Residual risk: User approves network to attacker's domain. Mitigation: Domain reputation checking, warnings for new/unknown domains.
Threat: Plugin crashes repeatedly, degrading experience
Mitigations:
Crash isolation: Worker restarts don't affect main app
Crash counting: After N crashes, plugin disabled
Timeouts: Long-running operations terminated
Fallback: Native rendering takes over
Threat: Plugin consumes excessive resources
Mitigations:
Memory limits: Wasm instance has memory cap
CPU limits: Execution time limits per operation
Storage quotas: Per-plugin storage limits
Rate limiting: Network requests, document writes rate-limited
Threat: Plugin performs clickjacking via iframe
Mitigations:
iframe sandboxed with restrictive attributes
Parent can't be navigated from iframe
UI rendered within Seed's frame, not overlayed
Threat: Plugin supply chain attack (compromised update)
Mitigations:
Capability versioning: New capabilities require re-consent
Update review: Changed capabilities highlighted to user
Rollback: Users can revert to previous plugin version
Signing: Plugins signed by developer key
15. SDK Design
15.1 TypeScript-First Approach
The primary SDK is TypeScript, optimized for developer experience:
import {
defineBlock,
defineAction,
defineService,
ui,
z,
} from '@seed/plugin-sdk';
// Full type inference from schemas
const schema = z.object({
title: z.string(),
count: z.number().min(0),
});
export const counterBlock = defineBlock({
name: 'counter',
schema,
// ctx.blockData is typed as { title: string; count: number }
async onRender(ctx) {
return ui.container({ direction: 'column' }, [
ui.heading(ctx.blockData.title, 2),
ui.text(`Count: ${ctx.blockData.count}`),
ui.button('Increment', { id: 'inc' }),
]);
},
async onEvent(ctx, event) {
if (event.type === 'click' && event.id === 'inc') {
await ctx.document.updateBlock(ctx.blockId, (data) => ({
...data,
count: data.count + 1, // Typed!
}));
}
},
});
SDK Features:
Type inference: Schemas generate TypeScript types automatically
IDE support: Autocomplete for all APIs, UI components
Validation: Build-time checking of manifest against code
Code splitting: SDK handles Wasm/iframe bundling
15.2 Lower-Level Language Support
Developers can write plugins in any language compiling to Wasm:
Rust:
use seed_plugin_sdk::prelude::*;
#[seed_block]
pub struct CounterBlock {
title: String,
count: u32,
}
impl Block for CounterBlock {
fn render(&self, ctx: &Context) -> UI {
ui::container(Direction::Column, vec![
ui::heading(&self.title, 2),
ui::text(&format!("Count: {}", self.count)),
ui::button("Increment").id("inc"),
])
}
fn on_event(&mut self, ctx: &mut Context, event: Event) {
if event.is_click("inc") {
self.count += 1;
ctx.update_block(self);
}
}
}
Go:
package main
import (
sdk "github.com/seed/plugin-sdk-go"
)
type CounterBlock struct {
Title string `json:"title"`
Count int `json:"count"`
}
func (b *CounterBlock) Render(ctx *sdk.Context) sdk.UI {
return sdk.Container(sdk.Column,
sdk.Heading(b.Title, 2),
sdk.Text(fmt.Sprintf("Count: %d", b.Count)),
sdk.Button("Increment").ID("inc"),
)
}
func (b *CounterBlock) OnEvent(ctx *sdk.Context, event sdk.Event) {
if event.IsClick("inc") {
b.Count++
ctx.UpdateBlock(b)
}
}
Lower-level SDKs provide the same capabilities with less automatic magic. Developers handle more serialization/deserialization themselves.
15.3 SDK Component Library
The UI components available in all SDKs:
namespace ui {
// Layout
function container(props: ContainerProps, children: Component[]): Component;
function divider(): Component;
function spacer(size: 'small' | 'medium' | 'large'): Component;
// Typography
function text(content: string, props?: TextProps): Component;
function heading(content: string, level: 1 | 2 | 3): Component;
// Form Controls
function textInput(props: TextInputProps): Component;
function textArea(props: TextAreaProps): Component;
function select(props: SelectProps): Component;
function checkbox(props: CheckboxProps): Component;
function toggle(props: ToggleProps): Component;
function slider(props: SliderProps): Component;
// Actions
function button(label: string, props?: ButtonProps): Component;
function buttonGroup(buttons: ButtonProps[]): Component;
// Display
function image(props: ImageProps): Component;
function icon(name: string, props?: IconProps): Component;
function badge(content: string, props?: BadgeProps): Component;
function progressBar(value: number, max: number): Component;
// Structure
function list(props: ListProps): Component;
function tabs(props: TabsProps): Component;
function accordion(props: AccordionProps): Component;
// Escape Hatch
function webView(props: WebViewProps): Component;
}
15.4 Build Toolchain
The SDK includes a build toolchain:
# Initialize a new plugin project
npx @seed/create-plugin my-plugin
# Development with hot reload
npm run dev
# Build for production
npm run build
# Outputs:
# - dist/manifest.json
# - dist/plugin.wasm
# - dist/ui/*.js (iframe bundles, if any)
# - dist/schemas/*.json
Build Steps:
TypeScript compilation: Type-check all code
Schema extraction: Generate JSON Schemas from Zod definitions
Code splitting: Separate Wasm code from iframe code
Wasm compilation: Compile plugin logic to .wasm via AssemblyScript or wasm-pack
Bundle optimization: Tree-shake, minify iframe bundles
Manifest generation: Produce final manifest.json
Validation: Check manifest against built artifacts
16. Wasm Instance Management
16.1 One Instance Per Plugin
Seed creates one Wasm instance per installed plugin, not per block or per surface. This balances memory usage with isolation:
Why not one instance per block?
50 diagram blocks = 50 Wasm instances = significant memory overhead
Startup time multiplied by block count
Excessive for common cases
Why not shared instances across plugins?
Plugins could interfere with each other
Crash in one plugin affects others
No capability isolation between plugins
One instance per plugin:
Reasonable memory footprint
Plugin-level isolation maintained
Single cold start per plugin per session
16.2 Block Instance Multiplexing
When a plugin renders multiple blocks, the host multiplexes through the single Wasm instance:
┌─────────────────────────────────────────────────────┐
│ │
│ Document with 5 diagram blocks │
│ │
│ Block A ──┐ │
│ Block B ──┤ │
│ Block C ──┼──► Single Wasm Instance ──► Renders │
│ Block D ──┤ (diagram plugin) │
│ Block E ──┘ │
│ │
└─────────────────────────────────────────────────────┘
Host responsibilities:
Track which blocks are rendered by which plugin
Route events to correct handlers with block ID
Pass correct block data for each render call
Handle concurrent requests (queue or parallelize)
Plugin responsibilities:
Handle onRender(ctx) where ctx.blockId identifies which block
Maintain internal state keyed by block ID (if needed)
Clean up when onUnmount(blockId) called
16.3 Memory and Lifecycle
Memory Limits:
Each Wasm instance has a memory cap (e.g., 256MB). If a plugin exceeds this:
Wasm traps (controlled crash)
Host receives error
Host disables plugin temporarily
User notified with option to restart or uninstall
Instance Lifecycle:
Plugin installed
│
▼
Instance created (lazy, on first use)
│
├──► Block rendered ──► Instance kept alive
│
├──► Action executed ──► Instance kept alive
│
├──► Idle timeout (5 min) ──► Instance terminated
│
└──► Plugin disabled/uninstalled ──► Instance terminated
Instances are created lazily (first use) and terminated after idle periods to conserve memory.
17. Timing and Loading
17.1 The Cold Start Problem
When a user opens a document with plugin-rendered blocks, the plugins must load before rendering:
User opens document
│
▼
Document loaded, blocks identified
│
├──► Native blocks: Render immediately
│
└──► Plugin blocks:
│
▼
Load plugin Wasm (50-200ms)
│
▼
Call onRender() (10-50ms)
│
▼
Render UI
This delay is visible to users. We mitigate it with loading states and schema defaults.
17.2 Schema Defaults and Skeletons
Schema-Based Skeleton:
While the plugin loads, Seed renders a skeleton based on the schema:
const schema = z.object({
title: z.string(),
items: z.array(z.string()),
chartType: z.enum(['bar', 'line', 'pie']),
});
Seed can render:
┌─────────────────────────────────────────┐
│ ████████████████ (title placeholder) │
│ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ (items placeholder) │
│ │
└─────────────────────────────────────────┘
Plugin-Declared Skeleton:
Plugins can provide custom loading UI:
export const chartBlock = defineBlock({
schema,
loadingUI: ui.container({ direction: 'column' }, [
ui.skeleton({ type: 'text', width: '60%' }),
ui.skeleton({ type: 'chart', height: 200 }),
]),
// ...
});
17.3 Lazy Loading Strategy
Viewport-Based Loading:
Only load plugins for blocks currently in (or near) the viewport:
┌──────────────────────────────────────────────────┐
│ │
│ ┌────────────────────┐ ◄── In viewport: │
│ │ Plugin Block A │ Plugin loaded │
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ ◄── Near viewport: │
│ │ Plugin Block B │ Plugin preloading │
│ └────────────────────┘ │
│ │
├──────────────────────────────────────────────────┤
│ │
│ ┌────────────────────┐ ◄── Far from viewport: │
│ │ Plugin Block C │ Not loaded yet │
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Plugin Block D │ Not loaded yet │
│ └────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
Preloading Heuristics:
Preload plugins for blocks near the scroll position
Preload plugins the user has recently interacted with
Preload plugins on mouse hover (anticipate interaction)
18. Plugin Manifest Specification
Complete manifest structure:
{
"$schema": "https://seed.dev/schemas/plugin-manifest-v1.json",
"id": "com.example.mermaid-diagrams",
"name": "Mermaid Diagrams",
"version": "1.2.0",
"description": "Render Mermaid diagrams in code blocks",
"author": {
"name": "Example Inc",
"email": "plugins@example.com",
"url": "https://example.com"
},
"license": "MIT",
"repository": "https://github.com/example/seed-mermaid",
"seed": {
"minVersion": "2.0.0",
"maxVersion": "3.x"
},
"capabilities": {
"network": [],
"storage": true,
"document": {
"read": "current-block",
"write": "current-block"
}
},
"surfaces": {
"mermaidBlock": {
"type": "block",
"extends": "code",
"when": { "language": "mermaid" },
"schema": "./schemas/mermaid.json",
"entry": "./dist/blocks/mermaid.wasm",
"render": "sandboxed",
"priority": 100
}
},
"migrations": {
"1.0.0→1.1.0": "./migrations/1.0-to-1.1.js",
"1.1.0→1.2.0": "./migrations/1.1-to-1.2.js"
},
"assets": [
"assets/icons/*.svg"
]
}
Fields:
| Field | Required | Description | |-------|----------|-------------| | id | Yes | Unique identifier (reverse domain) | | name | Yes | Display name | | version | Yes | SemVer version | | description | Yes | Short description | | author | Yes | Author information | | license | Yes | SPDX license identifier | | seed.minVersion | Yes | Minimum Seed version | | capabilities | Yes | Required permissions | | surfaces | Yes | Extension points | | migrations | No | Schema migration scripts | | assets | No | Additional files to include |
19. Detailed Examples
19.1 Example: Mermaid Diagram Block Extension
A plugin that renders Mermaid syntax in code blocks as diagrams.
manifest.json:
{
"id": "com.mermaid.seed-plugin",
"name": "Mermaid Diagrams",
"version": "1.0.0",
"capabilities": {
"storage": true,
"document": { "read": "current-block", "write": "current-block" }
},
"surfaces": {
"mermaidRenderer": {
"type": "block",
"extends": "code",
"when": { "language": "mermaid" },
"schema": "./schemas/mermaid.json",
"entry": "./dist/mermaid.wasm",
"render": "sandboxed"
}
}
}
schemas/mermaid.json:
{
"type": "object",
"properties": {
"code": { "type": "string" },
"language": { "const": "mermaid" },
"theme": { "enum": ["default", "dark", "forest", "neutral"], "default": "default" }
},
"required": ["code", "language"]
}
src/mermaid-block.ts:
import { defineBlock, ui, webView, z } from '@seed/plugin-sdk';
import mermaid from 'mermaid';
const schema = z.object({
code: z.string(),
language: z.literal('mermaid'),
theme: z.enum(['default', 'dark', 'forest', 'neutral']).default('default'),
});
export const mermaidBlock = defineBlock({
name: 'mermaid-renderer',
schema,
async onRender(ctx) {
const { code, theme } = ctx.blockData;
// Render mermaid to SVG in Wasm context
mermaid.initialize({ theme });
const { svg } = await mermaid.render('diagram', code);
return ui.container({ direction: 'column' }, [
// Show rendered diagram
ui.container({ className: 'diagram-container' }, [
ui.rawHtml(svg), // SVG rendered by host
]),
// Theme selector
ui.select({
id: 'theme',
label: 'Theme',
value: theme,
options: [
{ value: 'default', label: 'Default' },
{ value: 'dark', label: 'Dark' },
{ value: 'forest', label: 'Forest' },
{ value: 'neutral', label: 'Neutral' },
],
}),
// Toggle to show source
ui.toggle({ id: 'showSource', label: 'Show Source', enabled: false }),
]);
},
async onEvent(ctx, event) {
if (event.type === 'change' && event.id === 'theme') {
await ctx.document.updateBlock(ctx.blockId, (data) => ({
...data,
theme: event.value,
}));
}
},
});
User Experience:
User creates a code block with language "mermaid"
Without plugin: Seed shows syntax-highlighted mermaid code
With plugin installed: Seed renders the diagram visually
User can select themes, toggle source view
If plugin crashes: Falls back to syntax highlighting
19.2 Example: AI Writing Assistant Action
An action that improves selected text using an AI service.
manifest.json:
{
"id": "com.example.ai-writer",
"name": "AI Writing Assistant",
"version": "1.0.0",
"capabilities": {
"network": ["api.openai.com"],
"document": { "read": "current-page", "write": "current-page" }
},
"surfaces": {
"improveText": {
"type": "action",
"label": "Improve Writing",
"description": "Use AI to improve the selected text",
"icon": "magic-wand",
"shortcut": "Cmd+Shift+I",
"entry": "./dist/improve.wasm",
"parameters": {
"type": "object",
"properties": {
"style": {
"type": "string",
"enum": ["professional", "casual", "academic"],
"default": "professional"
}
}
}
}
}
}
src/improve-action.ts:
import { defineAction, ui, z } from '@seed/plugin-sdk';
const paramsSchema = z.object({
style: z.enum(['professional', 'casual', 'academic']).default('professional'),
});
export const improveAction = defineAction({
name: 'improve-text',
parameters: paramsSchema,
async onExecute(ctx, params) {
// Get selected text
const selection = await ctx.document.getSelection();
if (!selection || selection.text.length === 0) {
ctx.toast('Please select some text first', 'warning');
return;
}
// Show progress
ctx.showProgress('Improving text...', 0);
try {
// Call OpenAI API (host-mediated)
const response = await ctx.network.fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4',
messages: [{
role: 'user',
content: `Improve this text to be more ${params.style}:\n\n${selection.text}`,
}],
}),
});
ctx.showProgress('Improving text...', 50);
const data = await response.json();
const improvedText = data.choices[0].message.content;
// Replace selection with improved text
await ctx.document.replaceSelection(improvedText);
ctx.showProgress('Done!', 100);
ctx.toast('Text improved successfully', 'success');
} catch (error) {
ctx.toast(`Error: ${error.message}`, 'error');
}
},
});
User Experience:
User selects text in document
User invokes action (Cmd+Shift+I or command palette)
Parameter modal appears (if style not already chosen)
Progress indicator shows while AI processes
Selected text replaced with improved version
Can undo with Cmd+Z (Seed's native undo)
19.3 Example: Analytics Service
A background service that tracks document analytics and exposes an API to other plugins.
manifest.json:
{
"id": "com.example.analytics",
"name": "Document Analytics",
"version": "1.0.0",
"capabilities": {
"storage": true,
"document": { "read": "workspace" }
},
"surfaces": {
"analyticsService": {
"type": "service",
"provides": ["word-count", "reading-time", "complexity-score"],
"entry": "./dist/analytics.wasm"
},
"analyticsPage": {
"type": "page",
"path": "/plugins/analytics",
"label": "Analytics",
"icon": "chart-bar",
"navigation": "sidebar",
"entry": "./dist/page.wasm"
}
}
}
src/analytics-service.ts:
import { defineService, z } from '@seed/plugin-sdk';
export const analyticsService = defineService({
name: 'analytics',
methods: {
'word-count': {
input: z.object({ documentId: z.string() }),
output: z.object({ count: z.number() }),
async handler(input, ctx) {
const doc = await ctx.document.get(input.documentId);
const text = extractText(doc);
return { count: text.split(/\s+/).length };
},
},
'reading-time': {
input: z.object({ documentId: z.string(), wpm: z.number().default(200) }),
output: z.object({ minutes: z.number() }),
async handler(input, ctx) {
const { count } = await this.methods['word-count'].handler(
{ documentId: input.documentId },
ctx
);
return { minutes: Math.ceil(count / input.wpm) };
},
},
'complexity-score': {
input: z.object({ documentId: z.string() }),
output: z.object({ score: z.number(), grade: z.string() }),
async handler(input, ctx) {
const doc = await ctx.document.get(input.documentId);
const score = calculateFleschKincaid(doc);
const grade = scoreToGradeLevel(score);
return { score, grade };
},
},
},
});
Other plugins can call this service:
// In another plugin
const analytics = await ctx.plugins.call('analytics', 'reading-time', {
documentId: ctx.documentId,
wpm: 250,
});
console.log(`Reading time: ${analytics.minutes} minutes`);
19.4 Example: Full-Featured Plugin with Multiple Surfaces
A comprehensive task management plugin demonstrating multiple surfaces working together.
manifest.json:
{
"id": "com.example.tasks",
"name": "Task Manager",
"version": "2.0.0",
"capabilities": {
"network": ["api.example.com"],
"storage": true,
"document": { "read": "workspace", "write": "workspace" }
},
"surfaces": {
"taskBlock": {
"type": "block",
"blockType": "task",
"schema": "./schemas/task.json",
"entry": "./dist/blocks/task.wasm",
"render": "sandboxed"
},
"taskListBlock": {
"type": "block",
"blockType": "task-list",
"schema": "./schemas/task-list.json",
"entry": "./dist/blocks/task-list.wasm",
"render": "sandboxed"
},
"createTask": {
"type": "action",
"label": "Create Task",
"icon": "plus-circle",
"shortcut": "Cmd+Shift+T",
"entry": "./dist/actions/create.wasm"
},
"taskService": {
"type": "service",
"provides": ["task-sync"],
"entry": "./dist/services/sync.wasm"
},
"taskDashboard": {
"type": "page",
"path": "/plugins/tasks",
"label": "Tasks",
"icon": "check-square",
"navigation": "sidebar",
"entry": "./dist/pages/dashboard.wasm"
}
}
}
This plugin provides:
Task block: Inline task items in documents
Task list block: Aggregated task views
Create task action: Quick task creation from anywhere
Task service: Background sync with external task system
Task dashboard: Full-page task management view
All surfaces share the same Wasm instance and can communicate through internal state.
20. Comparison to Existing Systems
| Aspect | Seed | VS Code | Figma | WordPress | |--------|------|---------|-------|-----------| | Sandboxing | Wasm (strong) | None | Wasm (strong) | None | | UI Model | Declarative + iframe | Webview | iframe only | PHP templates | | Language | Any → Wasm | JS/TS | JS/TS | PHP | | Data Schema | Required | Optional | N/A | Optional | | Fallback | Always (native blocks) | N/A | N/A | Partial | | Permissions | Capability-based | None | Limited | None | | Platform | Web + Electron | Electron | Web | Server |
Seed's Advantages:
Security without sacrifice: Strong sandboxing without losing flexibility
Native-feeling UI: Declarative components render as native, not iframe
Graceful degradation: Schema-first design means content survives missing plugins
Extension points: Enhance native blocks rather than fragmenting the ecosystem
True cross-platform: Same plugins work web and desktop
Tradeoffs We Accept:
Message passing overhead: All plugin communication goes through the host
Wasm cold start: Initial plugin load takes 50-200ms
Limited iframe use: Full iframe access requires explicit opt-in and user warning
Schema rigidity: Plugins must define schemas upfront (can't be fully dynamic)
21. Open Questions and Future Considerations
Questions to Resolve
Plugin Marketplace: How do users discover and install plugins? Centralized store or decentralized?
Plugin Signing: Should plugins be cryptographically signed? By whom?
Revenue Sharing: Can plugin authors charge for plugins? What's the business model?
Debugging Experience: How do developers debug Wasm plugins? Source maps? Console integration?
Hot Reload: Can plugins reload without restarting Seed? During development?
Plugin Versioning: How do multiple versions coexist? Can users pin versions?
Future Considerations
AI Plugin Development: Could an AI assistant help users create simple plugins?
Visual Plugin Builder: No-code tools for creating simple extensions?
Plugin Composition: Can plugins extend other plugins' surfaces?
Collaborative Plugin State: Should plugins have shared state across collaborators?
Plugin Metrics: What telemetry should plugins have access to?
Deprecation Policy: How do we retire old plugin APIs?
22. Conclusion
Seed's plugin architecture represents a synthesis of lessons from across the software industry. By combining WebAssembly's security guarantees with a declarative UI layer, schema-first data design, and the extension point model, we achieve something genuinely new:
Plugins that are:
Secure by default (Wasm sandbox, capability-based permissions)
Beautiful by default (declarative UI, native rendering)
Portable by default (schemas, graceful degradation)
Discoverable by default (extension points, "open with" paradigm)
For developers:
TypeScript-first SDK with excellent ergonomics
Escape hatches for complex cases (lower-level languages, iframe UI)
Clear capability model (no guessing what's allowed)
Schema-driven development (types, validation, migration)
For users:
Plugins enhance, not fragment, the experience
Content remains accessible without plugins installed
Clear, contextual permission prompts
Consistent, native-feeling UI
This architecture positions Seed to have a thriving plugin ecosystem while maintaining the security, consistency, and reliability that collaborative document editing demands.
Document authored January 2026. Architecture subject to refinement based on implementation experience and community feedback.