Hosted onnoosphere.hyper.mediavia theHypermedia Protocol

Seed Hypermedia Plugin ArchitectureA Comprehensive Design Document

Seed Hypermedia Plugin Architecture

A Comprehensive Design Document

Version: 1.0 Draft Date: January 2026 Status: Architecture Specification

Table of Contents

  1. Executive Summary

  2. Prior Art Analysis

  3. The Fundamental Tradeoffs

  4. Design Requirements for Seed

  5. Core Architecture

  6. User Interface Architecture

  7. Extension Surface Types

  8. The Capability Model

  9. Schema-First Block Design

  10. The Extension Point Model

  11. Plugin-to-Plugin Communication

  12. Collaboration and Sync Integration

  13. Platform Considerations

  14. Security Model

  15. SDK Design

  16. Wasm Instance Management

  17. Timing and Loading

  18. Plugin Manifest Specification

  19. Detailed Examples

  20. Comparison to Existing Systems

  21. Open Questions and Future Considerations

  22. Conclusion

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:

  1. Main Thread Sandbox: Runs in QuickJS (a JavaScript engine compiled to WebAssembly). Has access to the Figma document API but no browser APIs.

  2. 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:

  1. Settings Stage: Startup configuration (mod settings that users can adjust)

  2. Prototype Stage: Defining game objects (recipes, machines, items, technologies)

  3. 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:

  1. Host application embeds a Wasm runtime (Wasmtime, Wasmer, etc.)

  2. Plugins compile to .wasm binaries from various languages

  3. Host loads plugins into sandboxed Wasm instances

  4. 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

  1. True Sandboxing: Plugins cannot escape their execution environment. No filesystem access, no unrestricted network, no access to other plugins' data without explicit permission.

  2. Cross-Platform Parity: The same plugins must work in both Electron (desktop) and web browser deployments. Behavior should be identical.

  3. Native-Feeling UI: Plugin UIs should be indistinguishable from Seed's native interface. No visual discontinuity, proper theming, correct accessibility.

  4. Graceful Degradation: Documents with plugin-created content must remain usable without the plugin installed. No data loss, no broken rendering.

  5. Schema-First Data: Plugin block data must be defined by schemas, enabling validation, indexing, and migration.

  6. Collaboration Compatibility: Plugins must integrate correctly with Seed's real-time sync and CRDT-based collaboration.

  7. TypeScript-First SDK: The primary development experience must be TypeScript with excellent types, IDE support, and documentation.

Should Have

  1. Multi-Language Support: Developers who prefer Rust, Go, or other languages should be able to write plugins with more effort.

  2. Extension Points over New Blocks: Plugins should enhance native blocks rather than creating parallel block types when possible.

  3. Granular Permissions: Users should grant specific capabilities, not blanket "allow everything" approval.

  4. Plugin-to-Plugin Communication: Plugins should be able to call each other's services through mediated channels.

Nice to Have

  1. Hot Reload: Plugin developers should be able to iterate without restarting Seed.

  2. Debuggability: Standard debugging tools should work with plugin code.

  3. 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:

  1. Multi-runtime support: Extism abstracts over Wasmtime, Wasmer, and browser Wasm

  2. Host function protocol: Standardized way to inject capabilities

  3. Memory management: Handles the complexity of passing data in/out of Wasm

  4. HTTP handling: Built-in support for host-mediated network requests

  5. 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:

  1. Wasm ↔ Main Thread: Structured messages through worker postMessage

  2. Main Thread ↔ iframe: Messages through iframe postMessage

  3. 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:

  1. Extracts webView.render functions into separate JavaScript bundles

  2. Compiles the rest to Wasm

  3. Sets up message passing plumbing automatically

  4. 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:

  1. Plugin declares desired capabilities in manifest

  2. User reviews and approves at install (or first use)

  3. Host injects only approved capabilities into Wasm environment

  4. 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:

  1. Declare domains in manifest:

{
  "capabilities": {
    "network": ["api.example.com", "cdn.example.com"]
  }
}
  1. User approves network access (with domain list shown)

  2. 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 ...' },
});
  1. 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:

  1. Detect block's schema version

  2. Load plugin's migration chain

  3. Apply migrations sequentially

  4. 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:

  1. Fallback exists: Without the plugin, native code/video blocks render normally

  2. Data portable: Block uses native schema (or extends it minimally)

  3. No fragmentation: Users don't have 5 different "code block" types

  4. 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:

  1. User's chosen plugin (if set)

  2. Most capable installed plugin (by plugin-declared priority)

  3. 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:

  1. Plugin calls ctx.document.updateBlock() with a transform function

  2. Host applies transform to current state

  3. Host validates result against schema

  4. Host submits to CRDT sync layer

  5. Sync layer handles conflicts, ordering, persistence

  6. 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:

  1. CSP restrictions: Limit iframe's fetch destinations

  2. Data minimization: Send only necessary data to iframe

  3. 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:

  1. TypeScript compilation: Type-check all code

  2. Schema extraction: Generate JSON Schemas from Zod definitions

  3. Code splitting: Separate Wasm code from iframe code

  4. Wasm compilation: Compile plugin logic to .wasm via AssemblyScript or wasm-pack

  5. Bundle optimization: Tree-shake, minify iframe bundles

  6. Manifest generation: Produce final manifest.json

  7. 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:

  1. Wasm traps (controlled crash)

  2. Host receives error

  3. Host disables plugin temporarily

  4. 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:

  1. User creates a code block with language "mermaid"

  2. Without plugin: Seed shows syntax-highlighted mermaid code

  3. With plugin installed: Seed renders the diagram visually

  4. User can select themes, toggle source view

  5. 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:

  1. User selects text in document

  2. User invokes action (Cmd+Shift+I or command palette)

  3. Parameter modal appears (if style not already chosen)

  4. Progress indicator shows while AI processes

  5. Selected text replaced with improved version

  6. 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:

  1. Security without sacrifice: Strong sandboxing without losing flexibility

  2. Native-feeling UI: Declarative components render as native, not iframe

  3. Graceful degradation: Schema-first design means content survives missing plugins

  4. Extension points: Enhance native blocks rather than fragmenting the ecosystem

  5. True cross-platform: Same plugins work web and desktop

Tradeoffs We Accept:

  1. Message passing overhead: All plugin communication goes through the host

  2. Wasm cold start: Initial plugin load takes 50-200ms

  3. Limited iframe use: Full iframe access requires explicit opt-in and user warning

  4. Schema rigidity: Plugins must define schemas upfront (can't be fully dynamic)

21. Open Questions and Future Considerations

Questions to Resolve

  1. Plugin Marketplace: How do users discover and install plugins? Centralized store or decentralized?

  2. Plugin Signing: Should plugins be cryptographically signed? By whom?

  3. Revenue Sharing: Can plugin authors charge for plugins? What's the business model?

  4. Debugging Experience: How do developers debug Wasm plugins? Source maps? Console integration?

  5. Hot Reload: Can plugins reload without restarting Seed? During development?

  6. Plugin Versioning: How do multiple versions coexist? Can users pin versions?

Future Considerations

  1. AI Plugin Development: Could an AI assistant help users create simple plugins?

  2. Visual Plugin Builder: No-code tools for creating simple extensions?

  3. Plugin Composition: Can plugins extend other plugins' surfaces?

  4. Collaborative Plugin State: Should plugins have shared state across collaborators?

  5. Plugin Metrics: What telemetry should plugins have access to?

  6. 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.

Do you like what you are reading?. Subscribe to receive updates.

Unsubscribe anytime