Hosted oneric.vicenti.netvia theHypermedia Protocol

    Seed Hypermedia Plugin Architecture

    A Comprehensive Design Document

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

    Table of Contents

    1. Executive Summary

      This document specifies the plugin architecture for Seed Hypermedia, a decentralized collaboration platform. The architecture synthesizes lessons from eight major extensibility systems (VS Code, Figma, Factorio, Chrome, Minecraft, Obsidian, WordPress, and WebAssembly/Extism) to create a system that achieves:

        Strong Security: True sandboxing via WebAssembly with capability-based permissions

        Excellent User Experience: Declarative UI that feels native, with iframe escape hatches for advanced cases

        Developer Ergonomics: TypeScript-first SDK with lower-level language support for power users

        Data Portability: Schema-first block design enabling graceful degradation and search indexing

        Platform Parity: Consistent behavior across Electron desktop app and web application

      The core insight driving this architecture is that the historical tradeoff between security and flexibility can be broken by combining WebAssembly's sandboxing with a declarative UI layer. Plugins run in isolated Wasm runtimes within worker threads, communicating exclusively via message passing. UI is rendered by the host from declarative descriptions, with iframe-based WebViews available as a composable escape hatch for complex visualizations.

      A key innovation is the Extension Point Model: rather than plugins defining entirely new block types, they extend native Seed blocks. A mermaid chart plugin doesn't create a "mermaid block" — it provides an extension point for the native code block when language=mermaid. This ensures fallback UI always exists and reduces fragmentation.

    2. Prior Art Analysis

      Before designing Seed's plugin architecture, we conducted extensive research into existing extensibility systems. Each represents different philosophies and tradeoffs that informed our decisions.

      2.1 VS Code Extensions

        Architecture Overview

        VS Code employs a multi-process architecture built on Electron. Extensions run in a dedicated Extension Host process, separate from the main UI rendering process. This separation ensures that extension code cannot block the editor's interface — a misbehaving extension might hang the Extension Host, but users can still type, save, and close files.

        The Extension Host is a Node.js process exposing the VS Code API. Extensions are JavaScript/TypeScript packages with a manifest (package.json) declaring:

          Activation Events: When the extension should load (on command, on language, on file type)

          Contribution Points: Static declarations for menus, commands, themes, keybindings

          Extension Dependencies: Other extensions required

        Key protocols enable language-agnostic tooling:

          Language Server Protocol (LSP): Separates language intelligence (completion, diagnostics) from the editor

          Debug Adapter Protocol (DAP): Standardizes debugger integration

        Security Model

        VS Code extensions are not sandboxed. The Extension Host has the same permissions as VS Code itself:

          Full filesystem access (read, write, delete any file)

          Unrestricted network requests

          Ability to spawn child processes

          Access to environment variables and system information

        The official documentation acknowledges this explicitly: "providing a scalable solution with full Node.js support is the reason for separating the extension process from the sandboxed Renderer Process."

        Security relies on external measures:

          Marketplace malware scanning before publication

          Publisher verification (domain ownership proof)

          Extension signing

          Community reporting

        Research has demonstrated these measures are insufficient. Security researchers have successfully published malicious extensions that bypass all checks using simple evasion techniques (detecting sandbox environments, delaying malicious behavior).

        Performance Characteristics

        VS Code's performance model is sophisticated:

          Lazy Loading: Extensions only activate when their activation events fire. An extension for Rust only loads when a .rs file opens.

          Process Isolation: Extension crashes don't take down the editor

          Contribution Point Separation: Static contributions (menus, themes) load without activating the extension

        What We Learned

        VS Code demonstrates that process isolation provides stability (crash protection, non-blocking UI) but not security. The lazy loading model via activation events is excellent and worth emulating. The LSP/DAP approach of standardized protocols for common functionality reduces redundant work.

        What We Avoid

        The complete lack of sandboxing is unacceptable for Seed. Users should not need to trust that extension authors are benevolent. The "marketplace scanning" security model provides an illusion of safety without substance.

      2.2 Figma Plugins

        Architecture Overview

        Figma employs a sophisticated dual-environment architecture specifically designed for security. Each plugin consists of two components:

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

          iframe UI: Runs standard browser JavaScript with full browser APIs (fetch, DOM, localStorage) but no document access.

        Communication between these environments uses postMessage, creating a clear security boundary.

        The evolution of this architecture is instructive. Figma initially used the Realms shim for sandboxing — a JavaScript-based approach to creating isolated execution contexts. Within weeks of launch, security researchers discovered vulnerabilities allowing sandbox escape. Figma quickly pivoted to QuickJS compiled to WebAssembly, their backup plan.

        QuickJS-to-Wasm is fundamentally more secure because:

          Plugin code runs in a completely separate JavaScript VM

          Object representations differ between sandbox and host (impossible to confuse them)

          WebAssembly itself is sandboxed with no direct browser API access

          All capabilities must be explicitly injected by the host

        Security Model

        Figma's model provides strong isolation:

          The sandbox cannot access browser APIs (no fetch, no DOM, no storage)

          The iframe cannot access the document (no reading designs, no modifications)

          Network requests from the iframe cannot exfiltrate document data (the iframe never sees it)

          The sandbox can only communicate outward through whitelisted APIs

        Manual review focuses on user experience, not security auditing — the sandbox provides the security guarantee.

        Performance Characteristics

        Running JavaScript through an interpreter compiled to WebAssembly introduces overhead compared to JIT-compiled JavaScript. Figma accepts this tradeoff; they note the interpreter is slower than regular JavaScript since it's not a JIT, but performance is acceptable.

        Key optimizations:

          Main thread execution for direct canvas access (no IPC for document operations)

          Lazy page loading (document pages load on demand)

          Explicit lifecycle management (plugins must call figma.closePlugin())

        UI Limitations

        The iframe approach has UX drawbacks:

          Styling discontinuity: Plugin UIs often look slightly "off" compared to Figma's native interface

          Focus handling: Tab order across iframe boundaries is problematic

          Drag and drop: Notoriously difficult across iframe boundaries

          Theme inheritance: Plugins can't easily inherit Figma's design tokens

        What We Learned

        Figma proves that strong sandboxing is achievable for browser-based applications. The QuickJS-to-Wasm approach provides real security guarantees, not security theater. The dual-environment model (sandbox for logic, iframe for UI) cleanly separates concerns.

        What We Improve

        Figma's iframe-only UI leads to inconsistent plugin experiences. By offering a declarative UI layer that Seed renders natively, we can achieve better visual integration while keeping iframes as an escape hatch.

      2.3 Factorio Modding System

        Architecture Overview

        Factorio uses a modified Lua 5.2 runtime for modding. The system operates in three distinct stages:

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

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

          Runtime Stage: Gameplay interaction through event handlers

        A critical constraint drives the entire design: multiplayer determinism. Factorio uses lockstep networking where all clients simulate identical game state. Every mod must produce identical results across different machines, or multiplayer desyncs occur.

        This constraint influences everything:

          pairs() was modified to iterate in insertion order (standard Lua has arbitrary order)

          math.random() uses a shared, seeded generator

          Functions and metatables cannot be serialized (only data persists in saves)

          The io and os modules are removed entirely

        Security Model

        Factorio's security model prioritizes determinism over isolation. Mods have significant game state access but operate within Lua's inherent limitations.

        Removed standard library modules:

          loadfile(), dofile() — no arbitrary file loading

          coroutine — could introduce timing variations

          io, os — filesystem and OS access

        The require() function only loads files from mod directories. Mods cannot access system files or network resources.

        Performance Characteristics

        Lua was chosen for its lightweight embedding and fast execution. The event-driven architecture enables efficient interaction — mods register handlers for specific events (on_entity_died, on_player_crafted_item) rather than polling.

        The prototype/runtime split means expensive operations (defining hundreds of recipes, loading graphics) happen once at load time, not during gameplay. Runtime code responds to events and manipulates existing objects.

        What We Learned

        Factorio demonstrates that a well-designed event system can provide significant flexibility even within a restricted environment. The staged loading model (settings → prototypes → runtime) cleanly separates concerns. The determinism constraints, while specific to games, illustrate how environmental requirements shape architecture.

        What Doesn't Apply

        Factorio's threat model differs from ours. They're protecting against accidental desyncs, not malicious code. Their users install mods knowing they're modifying their single-player or self-hosted game. Seed handles collaborative documents across organizations — the trust model is fundamentally different.

      2.4 Chrome Browser Extensions

        Architecture Overview

        Chrome extensions use a multi-component architecture:

          Service Workers: Background processing (replaced persistent background pages in Manifest V3)

          Content Scripts: Inject into web pages with limited privileges

          Popup/Options Pages: Extension UI

          Sandboxed Pages: For eval() and dynamic code execution

        Manifest V3 (the current extension format) introduced significant changes:

          Service workers replaced background pages (ephemeral, event-driven)

          Remote code execution banned (all code must be bundled)

          declarativeNetRequest replaced blocking webRequest (affecting ad blockers)

        Security Model

        Chrome uses a permission-based security model declared in manifest.json:

          Host permissions: Which URLs the extension can access

          API permissions: Specific Chrome APIs (tabs, storage, bookmarks, etc.)

          Content script permissions: Which pages to inject into

        Manifest V3 tightened security:

          Remote code banned — extensions cannot load JavaScript from external servers

          eval() prohibited except in designated sandbox pages

          Stricter Content Security Policy values

        Content scripts have reduced privileges:

          Can access page DOM but not the page's JavaScript context

          Cannot directly call Chrome APIs — must message the service worker

          Subject to the same-origin policy for network requests

        Performance Characteristics

        Service workers are ephemeral — they start on demand and shut down when idle. This reduces memory for inactive extensions but introduces cold start latency. Extensions must correctly handle the startup/shutdown lifecycle, persisting state to chrome.storage.

        The declarativeNetRequest API moves network filtering to the browser process, improving performance over JavaScript-based webRequest but reducing flexibility (rules are static, not programmable).

        Controversy

        Manifest V3 has been controversial. The Electronic Frontier Foundation called it "deceitful and threatening," arguing it restricts privacy tools and ad blockers to benefit Google's advertising business. The shift toward declarative APIs (safer, more performant) comes at the cost of programmatic flexibility.

        What We Learned

        Chrome demonstrates the tension between security constraints and developer/user expectations. The permission model is more explicit than VS Code's but still relies on user approval rather than technical enforcement. The service worker model shows how ephemeral execution reduces resource usage.

        What We Avoid

        Chrome's permission prompts train users to click "Allow" without reading. Seed's capability model should be more granular and contextual, with meaningful distinctions that users can understand.

      2.5 Minecraft Modding (Forge & Fabric)

        Architecture Overview

        Minecraft modding operates through mod loaders that intercept and modify the game's Java bytecode at runtime. Two dominant loaders exist:

        Forge (established 2011):

          Heavyweight, event-driven architecture

          Significantly modifies vanilla Minecraft code

          Comprehensive library of hooks and registries

          Mature ecosystem with thousands of mods

          Slower to update for new Minecraft versions

        Fabric (established 2018):

          Lightweight, minimalist design

          Uses Mixin for bytecode injection

          Base loader contains only essential hooks

          Faster updates for new Minecraft versions

          Growing ecosystem, particularly for performance mods

        Forge works by patching Minecraft's code at load time, inserting event dispatchers throughout. Mods subscribe to events (BlockBreakEvent, EntitySpawnEvent) and register content through registries.

        Fabric's Mixin framework allows direct bytecode injection. Developers specify injection points using annotations, and the framework weaves mod code into vanilla classes at runtime. This is more surgical but requires deeper understanding of Minecraft internals.

        Security Model

        Minecraft mods have no security boundary whatsoever. Mods are Java code running with full JVM permissions:

          Complete filesystem access

          Unrestricted network access

          Ability to execute system commands

          Full memory access within the JVM

          Can modify any game code or other mods

        The only protection is social: community trust, open-source code review, and distribution platform scanning. Malicious mods have caused real damage, including credential theft and cryptocurrency miners.

        Performance Characteristics

        Forge's heavyweight approach introduces baseline overhead but provides stability guarantees for complex modpacks with hundreds of mods. Its mature API reduces inter-mod conflicts.

        Fabric's minimal footprint offers faster startup and lower memory overhead. Mixin's direct bytecode modification can be more performant than event dispatch for certain operations.

        Both support large modpacks (100+ mods), though Forge historically handles complex dependencies better.

        What We Learned

        Minecraft demonstrates that users will accept significant security risks for powerful extensibility. The existence of both Forge (comprehensive but heavy) and Fabric (minimal but flexible) shows that different architectural philosophies serve different needs.

        What We Avoid

        The complete absence of sandboxing is unacceptable. Minecraft's trust model works only because users knowingly run local modifications to a game. Seed handles collaborative documents potentially containing sensitive information across organizational boundaries.

      2.6 Obsidian Plugins

        Architecture Overview

        Obsidian is an Electron application, and its plugins are JavaScript/TypeScript code running directly in the application context. Plugins have access to:

          The Obsidian API for note manipulation

          The Node.js runtime (full filesystem, network, process spawning)

          The DOM (can modify any UI element)

          Electron APIs (native dialogs, system information)

        The plugin architecture is straightforward: plugins are npm packages with a manifest.json, a main.js entry point, and optional styles.css. They load into the main Electron process with direct access to application internals.

        Security Model

        Obsidian's documentation is explicit: "Due to technical limitations in the plugin architecture, Obsidian cannot implement granular permission controls for community plugins. Instead, plugins inherit the full access level of the host application."

        This means plugins can:

          Read and modify any file on the user's filesystem

          Make arbitrary network requests

          Execute system commands

          Access encryption keys (since Obsidian offers encrypted sync)

          Potentially install persistent malware

        The only mitigation is community review before plugins appear in the official directory. This catches obvious issues but not sophisticated attacks.

        Performance Characteristics

        Plugins run in the main Electron process, so poorly-written plugins can block the UI. There's no lazy loading — enabled plugins load at startup. Performance varies wildly by plugin quality.

        What We Learned

        Obsidian's architecture is essentially identical to VS Code's — both are Electron applications with unsandboxed JavaScript plugins. The honesty of Obsidian's documentation about security limitations is refreshing, but the limitations themselves are concerning.

        What We Avoid

        Seed requires real security, not trust-based security theater. Users collaborating on documents should not need to worry that a teammate's installed plugin might exfiltrate data.

      2.7 WordPress Plugin Architecture

        Architecture Overview

        WordPress uses a hook-based plugin architecture implemented entirely in PHP. The system revolves around two types of hooks:

        Actions: Execute code at specific points

        add_action('wp_head', 'my_custom_function');
        
        function my_custom_function() {
            echo '<meta name="custom" content="value">';
        }
        

        Filters: Modify data before it's used

        add_filter('the_content', 'modify_content');
        
        function modify_content($content) {
            return $content . '<p>Added by plugin</p>';
        }
        

        WordPress core (and themes) trigger hooks using do_action() and apply_filters(). A page request flows through hundreds of hooks, each providing an opportunity for plugins to inject behavior or modify content.

        The WP_Hook class manages all registrations, storing callbacks with priorities that determine execution order.

        Security Model

        WordPress plugins have full server-side PHP execution capabilities:

          Direct database access

          Read/write any file the web server can access

          Outbound network requests

          Execute system commands (if not disabled by hosting)

        Security relies on:

          Code review for wordpress.org hosted plugins

          Capability checks within plugin code

          Nonce verification for form submissions

          Prepared SQL statements

        These are developer responsibilities, not enforced boundaries. Poorly-written plugins are a primary attack vector for WordPress sites, responsible for the majority of WordPress security breaches.

        Performance Characteristics

        Every active plugin adds overhead to every page request. Plugins registering many hooks or implementing expensive callbacks significantly impact performance. WordPress lacks lazy loading — all active plugins initialize on every request.

        Caching (page, object, opcode) is the primary mitigation, but poorly-optimized plugins can defeat caching strategies.

        What We Learned

        WordPress's hook system is elegantly simple: register callbacks to named hooks, and they execute at the right time. The action/filter distinction (side effects vs. data transformation) is a useful conceptual model.

        The WordPress ecosystem demonstrates that simple extensibility primitives can enable a massive plugin economy. The existence of plugins like WooCommerce shows that hook-based systems can support complex functionality.

        What We Avoid

        WordPress's complete lack of isolation makes it unsuitable as a security model. The server-side PHP execution model doesn't translate to our client-side/Wasm architecture anyway.

      2.8 WebAssembly-Based Systems (Extism)

        Architecture Overview

        WebAssembly represents a new paradigm for plugin systems. Extism is a framework for building Wasm-based extensibility, providing:

          Host SDKs: Embed Wasm runtime in applications (Rust, Go, Node.js, Python, Ruby, etc.)

          Plugin Development Kits (PDKs): Write plugins in any language that compiles to Wasm

          Standard interface: Common protocol for host-plugin communication

        The architecture:

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

          Plugins compile to .wasm binaries from various languages

          Host loads plugins into sandboxed Wasm instances

          Communication occurs through well-defined interfaces (Wasm imports/exports)

        WebAssembly Interface Types (WIT) define contracts between host and plugins with strong typing that goes beyond C's limitations.

        Security Model

        WebAssembly's security is fundamentally different from traditional plugin systems:

        Capability-Based Security: By default, Wasm modules cannot access anything. The host explicitly grants capabilities:

          Filesystem access (if any) is per-directory

          Network access (if any) is per-domain

          No implicit access to host memory, environment, or system calls

        Sandboxed Execution:

          Wasm memory is isolated from host memory

          Call stack is protected (separate from data memory)

          Plugins cannot access host code or other plugins' memory

          Crashes are contained within the Wasm instance

        Interface Enforcement:

          Communication only through typed interfaces

          No arbitrary memory access or function calls

          Host controls exactly what the plugin can do

        Performance Characteristics

        WebAssembly approaches native performance through AOT or JIT compilation. Research shows Wasm is approximately 2.3× slower than native code for typical workloads, excluding cases where native code benefits from hardware-specific instructions.

        Startup time is significantly faster than containers or VMs — Wasm instances spin up in milliseconds. Memory overhead per instance is minimal.

        Real-World Adoption

        WebAssembly plugins are used in production by:

          Shopify: Merchant extensions in any language, sandboxed execution

          Envoy/Istio: Proxy filters for service mesh

          Goldman Sachs: API gateway customization

          Apache HTTP Server: mod_wasm for request handling

          Cloudflare Workers: Edge computing with Wasm isolates

        What We Learned

        WebAssembly solves the security-flexibility tradeoff that plagues other systems. True sandboxing with explicit capabilities, language-agnostic development, and near-native performance represent a genuine advance.

        Extism specifically provides the infrastructure we need: host SDKs for both browser (JavaScript) and Electron (Node.js), PDKs for TypeScript and other languages, and a proven model for capability injection.

        What We Build Upon

        Seed's plugin architecture uses WebAssembly (via Extism) as its foundation. The sandboxing model, capability-based security, and host-mediated communication are exactly what we need. We extend this foundation with our declarative UI layer, schema-first blocks, and extension point model.

    3. The Fundamental Tradeoffs

      Plugin system design involves navigating a trilemma:

                          SECURITY
                             ▲
                            /|\
                           / | \
                          /  |  \
                         /   |   \
                        /    |    \
                       /     |     \
                      /      |      \
                     /       |       \
                    /________|________\
               FLEXIBILITY          PERFORMANCE
      

      Security (isolation, sandboxing, capability enforcement)

        Strong: Figma, WebAssembly

        Weak: VS Code, Obsidian, Minecraft, WordPress

      Flexibility (what plugins can do)

        High: VS Code, Minecraft, WordPress

        Constrained: Figma, Factorio

      Performance (overhead, latency, resource usage)

        Optimized: VS Code (lazy loading), Chrome (service workers)

        Overhead: Figma (interpreter), sandboxed systems generally

      Historical Constraints

        Traditionally, you could pick two:

        Security + Performance → Limited Flexibility Chrome's Manifest V3 moves toward declarative APIs that are secure and performant but restrict what extensions can do.

        Flexibility + Performance → No Security VS Code and Minecraft provide maximum capability with minimal overhead but zero isolation.

        Security + Flexibility → Performance Cost Figma's QuickJS-to-Wasm approach is secure and reasonably capable but incurs interpreter overhead.

      Breaking the Tradeoff

        WebAssembly changes this equation:

          Security: Wasm's sandbox is baked into the virtual machine design, not a bolted-on afterthought

          Performance: JIT/AOT compilation achieves near-native speed

          Flexibility: Any language can compile to Wasm; host can expose rich capabilities

        The remaining constraint is communication overhead. If plugins need to make thousands of fine-grained calls per frame, message passing latency accumulates. This is acceptable for Seed's use case (document editing, not real-time game simulation).

    4. Design Requirements for Seed

      Based on our analysis of existing systems and Seed's specific needs, we established these requirements:

      Must Have

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

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

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

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

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

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

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

      Should Have

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

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

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

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

      Nice to Have

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

        Debuggability: Standard debugging tools should work with plugin code.

        Analytics/Telemetry Hooks: Plugin authors should be able to understand usage patterns without building their own infrastructure.

    5. Core Architecture

      5.1 High-Level Overview

        Seed's plugin architecture consists of three layers:

        ┌─────────────────────────────────────────────────────────────────────┐
        │                        SEED APPLICATION                              │
        │                                                                      │
        │  ┌──────────────────────────────────────────────────────────────┐   │
        │  │                      MAIN THREAD                              │   │
        │  │                                                               │   │
        │  │   ┌─────────────┐  ┌─────────────┐  ┌────────────────────┐   │   │
        │  │   │   Seed UI   │  │ Declarative │  │   iframe WebViews  │   │   │
        │  │   │   (React)   │  │  Plugin UI  │  │   (escape hatch)   │   │   │
        │  │   └─────────────┘  └─────────────┘  └────────────────────┘   │   │
        │  │          │                │                    │              │   │
        │  │          └────────────────┼────────────────────┘              │   │
        │  │                           │                                    │   │
        │  │                    Message Passing                             │   │
        │  │                           │                                    │   │
        │  └───────────────────────────┼────────────────────────────────────┘   │
        │                              │                                        │
        │  ┌───────────────────────────┼────────────────────────────────────┐   │
        │  │                    WORKER THREAD(S)                            │   │
        │  │                           │                                    │   │
        │  │   ┌───────────────────────▼───────────────────────────────┐   │   │
        │  │   │              WASM RUNTIME (Extism)                     │   │   │
        │  │   │                                                        │   │   │
        │  │   │   ┌──────────┐  ┌──────────┐  ┌──────────┐            │   │   │
        │  │   │   │ Plugin A │  │ Plugin B │  │ Plugin C │   ...      │   │   │
        │  │   │   │  .wasm   │  │  .wasm   │  │  .wasm   │            │   │   │
        │  │   │   └──────────┘  └──────────┘  └──────────┘            │   │   │
        │  │   │                                                        │   │   │
        │  │   │   ┌────────────────────────────────────────────────┐  │   │   │
        │  │   │   │           CAPABILITY LAYER                      │  │   │   │
        │  │   │   │   Network │ Storage │ Document │ UI │ ...      │  │   │   │
        │  │   │   └────────────────────────────────────────────────┘  │   │   │
        │  │   └────────────────────────────────────────────────────────┘   │   │
        │  │                                                                │   │
        │  └────────────────────────────────────────────────────────────────┘   │
        │                                                                       │
        │  ┌────────────────────────────────────────────────────────────────┐   │
        │  │               ELECTRON ONLY: NODE.JS BACKEND                   │   │
        │  │                (optional, for native integrations)              │   │
        │  └────────────────────────────────────────────────────────────────┘   │
        │                                                                       │
        └───────────────────────────────────────────────────────────────────────┘
        

      5.2 WebAssembly Runtime with Extism

        We use Extism as our Wasm runtime framework. Extism provides:

        Host SDK (runs in our worker):

          Loads .wasm plugin binaries

          Creates isolated Wasm instances

          Injects host functions (capabilities) into the Wasm environment

          Manages memory and handles data marshaling

          Enforces resource limits (memory, execution time)

        Plugin Development Kit (used by plugin authors):

          Abstracts Wasm's low-level details

          Provides idiomatic APIs for each supported language

          Handles serialization/deserialization of complex types

          Exposes host-provided capabilities

        Why Extism over raw Wasm runtimes:

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

          Host function protocol: Standardized way to inject capabilities

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

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

          Battle-tested: Used in production by Shopify, among others

      5.3 Worker-Based Execution Model

        Plugin Wasm code runs in Web Workers (browser) or Worker Threads (Node.js/Electron). This provides:

        Non-Blocking UI: The main thread remains responsive regardless of plugin behavior. A plugin stuck in an infinite loop cannot freeze the editor.

        Crash Isolation: A plugin crash terminates only its worker, not the entire application.

        Security Boundary: Workers have limited access to the main thread's context. Combined with Wasm sandboxing, this creates defense in depth.

        Architecture by Platform:

        | Context | Web Browser | Electron | |---------|-------------|----------| | UI Extensions | Web Worker | Web Worker (renderer process) | | Services | Web Worker | Worker Thread (can use Node.js APIs) | | Document Access | Via main thread message | Via main thread message |

      5.4 Message Passing Protocol

        All communication between plugins and the host uses structured message passing:

        // Host → Plugin
        interface HostMessage {
          type: 'invoke' | 'event' | 'response';
          id: string;           // Correlation ID for request/response
          surface: string;      // Which plugin surface (block, action, etc.)
          payload: unknown;     // Typed by surface schema
        }
        
        // Plugin → Host  
        interface PluginMessage {
          type: 'request' | 'response' | 'ui-update';
          id: string;
          payload: unknown;
        }
        

        Message Types:

          invoke: Host calls a plugin function (render block, execute action)

          event: Host notifies plugin of something (state changed, user interaction)

          response: Reply to a previous request

          request: Plugin asks host to do something (fetch URL, write document)

          ui-update: Plugin sends new declarative UI description

        The protocol is symmetric and asynchronous. All operations that might take time return Promises resolved via response messages.

    6. User Interface Architecture

      6.1 The Declarative UI Foundation

        Most plugin UIs don't need arbitrary HTML/CSS/JavaScript. They need forms, lists, buttons, and status displays. By providing a declarative component vocabulary, Seed can render plugin UIs natively, ensuring:

          Visual Consistency: Plugin UIs inherit Seed's design system automatically

          Theme Support: Dark mode, custom themes work without plugin author effort

          Accessibility: Screen reader support, keyboard navigation built-in

          Focus Management: Tab order flows correctly through the application

          Performance: No iframe overhead for simple UIs

        Core Component Vocabulary:

        type SeedUIComponent =
          // Layout
          | { type: 'container'; direction: 'row' | 'column'; children: SeedUIComponent[] }
          | { type: 'divider' }
          | { type: 'spacer'; size: 'small' | 'medium' | 'large' }
          
          // Typography
          | { type: 'text'; content: string; variant?: 'body' | 'caption' | 'code' }
          | { type: 'heading'; content: string; level: 1 | 2 | 3 }
          
          // Form Controls
          | { type: 'textInput'; id: string; label: string; placeholder?: string; value?: string }
          | { type: 'textArea'; id: string; label: string; rows?: number; value?: string }
          | { type: 'select'; id: string; label: string; options: Array<{value: string; label: string}>; value?: string }
          | { type: 'checkbox'; id: string; label: string; checked?: boolean }
          | { type: 'toggle'; id: string; label: string; enabled?: boolean }
          | { type: 'slider'; id: string; label: string; min: number; max: number; value?: number }
          
          // Actions
          | { type: 'button'; id: string; label: string; variant?: 'primary' | 'secondary' | 'danger' }
          | { type: 'buttonGroup'; children: Array<{ type: 'button'; id: string; label: string }> }
          
          // Display
          | { type: 'image'; src: string; alt: string; width?: number; height?: number }
          | { type: 'icon'; name: string; size?: 'small' | 'medium' | 'large' }
          | { type: 'badge'; content: string; variant?: 'info' | 'success' | 'warning' | 'error' }
          | { type: 'progressBar'; value: number; max: number }
          
          // Structure
          | { type: 'list'; items: Array<{ id: string; primary: string; secondary?: string; icon?: string }> }
          | { type: 'tabs'; id: string; tabs: Array<{ id: string; label: string; content: SeedUIComponent[] }> }
          | { type: 'accordion'; sections: Array<{ id: string; title: string; content: SeedUIComponent[] }> }
          
          // Escape Hatch
          | { type: 'webView'; id: string; src: string; height: number };
        

        This vocabulary covers ~95% of plugin UI needs. This approach follows successful prior art:

          Raycast: Uses React-like components (<List>, <Form>, <Detail>) that render as native macOS UI. Developers literally cannot make an ugly plugin.

          Slack Block Kit: Declarative JSON describes messages and modals. Slack renders them consistently.

          VS Code Contribution Points: Static JSON declares menus, commands, settings — VS Code renders them natively.

        Plugins describe their UI declaratively:

        const ui: SeedUIComponent = {
          type: 'container',
          direction: 'column',
          children: [
            { type: 'heading', content: 'Import Settings', level: 2 },
            { type: 'textInput', id: 'url', label: 'Source URL', placeholder: 'https://...' },
            { type: 'select', id: 'format', label: 'Format', options: [
              { value: 'json', label: 'JSON' },
              { value: 'csv', label: 'CSV' },
            ]},
            { type: 'checkbox', id: 'overwrite', label: 'Overwrite existing data' },
            { type: 'divider' },
            { type: 'buttonGroup', children: [
              { type: 'button', id: 'cancel', label: 'Cancel', variant: 'secondary' },
              { type: 'button', id: 'import', label: 'Import', variant: 'primary' },
            ]},
          ],
        };
        

        Seed renders this using its native React components. The plugin never touches the DOM.

      6.2 The iframe Escape Hatch

        Some plugins genuinely need capabilities beyond declarative UI:

          Custom Visualizations: WebGL, Canvas, D3.js charts

          Third-Party Embeds: YouTube players, map widgets

          Rich Text Editing: CodeMirror, Monaco editor

          Real-Time Collaboration: Whiteboard, collaborative drawing

        For these cases, the webView component embeds an iframe:

        const ui: SeedUIComponent = {
          type: 'container',
          direction: 'column',
          children: [
            { type: 'heading', content: '3D Model Preview', level: 2 },
            { type: 'webView', id: 'preview', src: 'plugin://model-viewer/preview.html', height: 400 },
            { type: 'button', id: 'import', label: 'Import to Document', variant: 'primary' },
          ],
        };
        

        The iframe loads content bundled with the plugin. It has browser APIs (Canvas, WebGL, fetch) but runs in a null-origin sandbox.

      6.3 Composing Declarative UI with iframes

        The key insight — and what makes this architecture elegant — is that iframes are leaf nodes in the declarative tree, not a separate mode. This means:

          The host controls overall layout, padding, chrome

          Focus flows naturally (host manages entering/leaving the iframe)

          Native components surround the iframe seamlessly

          Plugin authors use one component (webView) instead of rebuilding everything

        Example: Video Editor Plugin

        const ui: SeedUIComponent = {
          type: 'container',
          direction: 'column',
          children: [
            { type: 'heading', content: 'Video Trimmer', level: 2 },
            
            // Declarative: native Seed components
            { type: 'textInput', id: 'title', label: 'Video Title', value: 'Untitled' },
            
            // Escape hatch: custom video timeline UI
            { type: 'webView', id: 'timeline', src: 'plugin://video-editor/timeline.html', height: 200 },
            
            // Declarative again
            { type: 'container', direction: 'row', children: [
              { type: 'text', content: 'Start: ' },
              { type: 'textInput', id: 'startTime', label: '', value: '00:00' },
              { type: 'text', content: 'End: ' },
              { type: 'textInput', id: 'endTime', label: '', value: '00:30' },
            ]},
            
            { type: 'button', id: 'apply', label: 'Apply Trim', variant: 'primary' },
          ],
        };
        

        The video timeline (a complex interactive canvas) uses the escape hatch, while everything else is native.

      6.4 The Three-Context Model

        With iframes in the picture, plugins span three execution contexts:

        ┌───────────────────────────────────────────────────────────────────────────┐
        │                                                                           │
        │  CONTEXT 1: Worker Thread                                                 │
        │  ┌─────────────────────────────────────────────────────────────────────┐  │
        │  │  Wasm Plugin Code                                                    │  │
        │  │  - Business logic                                                    │  │
        │  │  - Document manipulation                                             │  │
        │  │  - State management                                                  │  │
        │  │  - Produces declarative UI descriptions                              │  │
        │  └─────────────────────────────────────────────────────────────────────┘  │
        │       │                                                                    │
        │       │ postMessage (structured data)                                      │
        │       ▼                                                                    │
        │  CONTEXT 2: Main Thread                                                   │
        │  ┌─────────────────────────────────────────────────────────────────────┐  │
        │  │  Seed Host Application                                               │  │
        │  │  - Renders declarative UI using React                                │  │
        │  │  - Creates iframe elements for webView components                    │  │
        │  │  - Routes events back to Wasm                                        │  │
        │  │  - Mediates all communication                                        │  │
        │  └─────────────────────────────────────────────────────────────────────┘  │
        │       │                                                                    │
        │       │ postMessage (to iframe)                                            │
        │       ▼                                                                    │
        │  CONTEXT 3: iframe (when webView used)                                    │
        │  ┌─────────────────────────────────────────────────────────────────────┐  │
        │  │  Plugin UI Code (JavaScript)                                         │  │
        │  │  - Custom rendering (Canvas, WebGL, DOM)                             │  │
        │  │  - Browser APIs (fetch, localStorage*)                               │  │
        │  │  - Handles local interactions                                        │  │
        │  │  - Sends significant events back to main thread                      │  │
        │  │                                                                       │  │
        │  │  * localStorage scoped to plugin origin                               │  │
        │  └─────────────────────────────────────────────────────────────────────┘  │
        │                                                                           │
        └───────────────────────────────────────────────────────────────────────────┘
        

        Communication Paths:

          Wasm ↔ Main Thread: Structured messages through worker postMessage

          Main Thread ↔ iframe: Messages through iframe postMessage

          Wasm ↔ iframe: No direct path. All communication routes through main thread.

        This architecture ensures the host can audit, rate-limit, and control all communication.

      6.5 Unified SDK Authoring

        Plugin authors shouldn't need to think about the three contexts. The TypeScript SDK provides a unified authoring experience:

        // youtube-block.ts — single file, SDK handles the split
        
        import { defineBlock, ui, webView } from '@seed/plugin-sdk';
        
        export const youtubeBlock = defineBlock({
          name: 'youtube-embed',
          schema: z.object({
            videoId: z.string(),
            startTime: z.number().optional(),
          }),
          
          // Runs in Wasm context
          async onRender(ctx) {
            const { videoId, startTime } = ctx.blockData;
            
            return ui.container({ direction: 'column' }, [
              ui.heading('YouTube Video', 2),
              
              // This webView is authored inline but extracted to iframe bundle
              webView({
                id: 'player',
                height: 315,
                
                // This function runs in iframe context
                render: ({ onMessage, sendMessage }) => {
                  const container = document.createElement('div');
                  
                  // Use YouTube iframe API
                  const player = new YT.Player(container, {
                    videoId: ctx.blockData.videoId,  // Captured from outer scope
                    playerVars: { start: ctx.blockData.startTime || 0 },
                    events: {
                      onStateChange: (e) => sendMessage({ type: 'state', state: e.data }),
                    },
                  });
                  
                  onMessage('seek', (time) => player.seekTo(time));
                  
                  return container;
                },
              }),
              
              ui.button('Copy Link', { id: 'copy', variant: 'secondary' }),
            ]);
          },
          
          // Runs in Wasm context
          async onEvent(ctx, event) {
            if (event.type === 'click' && event.id === 'copy') {
              const url = `https://youtube.com/watch?v=${ctx.blockData.videoId}`;
              await ctx.clipboard.write(url);
              ctx.toast('Link copied!');
            }
          },
        });
        

        The SDK build toolchain:

          Extracts webView.render functions into separate JavaScript bundles

          Compiles the rest to Wasm

          Sets up message passing plumbing automatically

          Handles data serialization between contexts

    7. Extension Surface Types

      Seed plugins can register multiple surfaces — distinct ways of extending the application. Each surface type has specific capabilities and constraints.

      7.1 Block UI Extensions

        Block extensions render custom block types or provide alternative renderers for existing blocks.

        Manifest Declaration:

        {
          "surfaces": {
            "mermaidRenderer": {
              "type": "block",
              "extends": "code",
              "when": { "language": "mermaid" },
              "schema": "./schemas/mermaid.json",
              "entry": "blocks/mermaid.wasm"
            },
            "customDiagram": {
              "type": "block",
              "blockType": "custom-diagram",
              "schema": "./schemas/diagram.json",
              "entry": "blocks/diagram.wasm"
            }
          }
        }
        

        Capabilities:

          Read the block's data (validated against schema)

          Return declarative UI (with optional webView)

          Respond to user interactions

          Request document writes (validated, persisted by host)

        Lifecycle:

        Block appears in viewport
                 │
                 ▼
            Load plugin Wasm (if not loaded)
                 │
                 ▼
            Call onRender(blockData) ──────► Returns UI description
                 │
                 ▼
            Host renders UI
                 │
                 ├──► User interacts ──► onEvent(event) ──► May return new UI
                 │
                 ├──► Block data changes (collab edit) ──► onRender(newData)
                 │
                 └──► Block leaves viewport ──► onUnmount() (cleanup)
        

      7.2 Action Extensions

        Actions are commands users can invoke. They appear in command palettes, context menus, or keyboard shortcuts.

        Manifest Declaration:

        {
          "surfaces": {
            "formatDocument": {
              "type": "action",
              "label": "Format Document",
              "description": "Apply consistent formatting to the document",
              "icon": "format-align-left",
              "shortcut": "Cmd+Shift+F",
              "parameters": {
                "type": "object",
                "properties": {
                  "style": { "enum": ["compact", "spaced", "academic"] }
                }
              },
              "entry": "actions/format.wasm"
            }
          }
        }
        

        Capabilities:

          Receive invocation with typed parameters

          Read document content (with permission)

          Write document changes (with permission)

          Show progress UI

          Return results

        Execution Flow:

        User triggers action (menu, shortcut, command palette)
                 │
                 ▼
            Parameter collection (if parameters defined)
                 │
                 ▼
            Call onExecute(params, context)
                 │
                 ├──► Plugin reads document
                 │
                 ├──► Plugin performs computation
                 │
                 ├──► Plugin requests writes ──► Host validates & applies
                 │
                 └──► Plugin returns result ──► Host shows completion
        

      7.3 Service Extensions

        Services are long-running background processes that respond to events or provide capabilities to other plugins.

        Manifest Declaration:

        {
          "surfaces": {
            "aiService": {
              "type": "service",
              "provides": ["text-completion", "summarization"],
              "capabilities": {
                "network": ["api.openai.com"]
              },
              "entry": "services/ai.wasm"
            }
          }
        }
        

        Capabilities:

          Run continuously (within worker lifecycle)

          Respond to requests from other plugins (host-mediated)

          Subscribe to document events

          Make network requests (to permitted domains)

        Use Cases:

          AI/ML inference services

          Real-time data sync with external systems

          Background indexing or processing

          Shared utilities for other plugins

      7.4 UI Page Extensions

        Page extensions add new pages/views to Seed's navigation, like settings panels or dashboards.

        Manifest Declaration:

        {
          "surfaces": {
            "analyticsPage": {
              "type": "page",
              "path": "/plugins/analytics",
              "label": "Analytics Dashboard",
              "icon": "chart-bar",
              "navigation": "sidebar",
              "entry": "pages/analytics.wasm"
            }
          }
        }
        

        Capabilities:

          Full page rendering (declarative UI + webView)

          Query parameters passed to plugin

          Navigation integration (sidebar, tabs)

          Deep linking support

    8. The Capability Model

      8.1 Capability-Based Security

        Unlike permission-based models (where users approve capabilities at install time), capability-based security means the host explicitly provides each capability to plugins. Plugins cannot discover or access capabilities they haven't been granted.

        In practice:

          Plugin declares desired capabilities in manifest

          User reviews and approves at install (or first use)

          Host injects only approved capabilities into Wasm environment

          Plugin cannot access unapproved capabilities — they don't exist in its environment

        This is fundamentally more secure than permission prompts because:

          Plugins can't try to access things they shouldn't

          There's no API to even attempt forbidden operations

          Security boundary is enforced by the Wasm VM, not application code

      8.2 Network Access

        By default, plugins cannot make network requests. The Wasm sandbox has no fetch, no XMLHttpRequest, no WebSocket.

        Plugins that need network access:

          Declare domains in manifest:

        {
          "capabilities": {
            "network": ["api.example.com", "cdn.example.com"]
          }
        }
        

          User approves network access (with domain list shown)

          Host injects http_request function into Wasm:

        // Inside Wasm, plugin calls:
        const response = await Host.httpRequest({
          method: 'GET',
          url: 'https://api.example.com/data',
          headers: { 'Authorization': 'Bearer ...' },
        });
        

          Host intercepts, validates, and performs the request:

            Checks URL against approved domains

            Can inject authentication headers

            Can rate-limit requests

            Logs all network activity

        Why host-mediated networking:

          Auditability: Host knows exactly what plugins communicate

          Rate limiting: Prevents plugins from overwhelming APIs

          Auth injection: Plugins don't need to store credentials

          Domain enforcement: Can't be bypassed by clever URL construction

      8.3 Document Access

        Document access is the most sensitive capability for Seed. Plugins can request:

        Read Access:

          document:read:current-block — Only the block they're rendering

          document:read:current-page — All blocks on current page

          document:read:workspace — Entire workspace (sensitive!)

        Write Access:

          document:write:current-block — Modify their own block's data

          document:write:current-page — Create/modify blocks on current page

          document:write:workspace — Create/modify anything (very sensitive!)

        Access Flow:

        // Plugin requests document content
        const page = await ctx.document.getCurrentPage();
        
        // Host checks:
        // 1. Does plugin have document:read:current-page capability?
        // 2. Is user authenticated with access to this page?
        // 3. Are there any collaboration locks?
        
        // If approved, host returns sanitized document data
        

        Write Validation:

        All document writes go through the host:

        // Plugin requests a write
        await ctx.document.updateBlock(blockId, newData);
        
        // Host validates:
        // 1. Does plugin have write capability for this scope?
        // 2. Does newData match the block's schema?
        // 3. Is the write valid per CRDT rules?
        // 4. Apply through sync layer (handles conflicts)
        

      8.4 Storage

        Plugins can store persistent data scoped to themselves:

        // Simple key-value storage
        await ctx.storage.set('preferences', { theme: 'dark' });
        const prefs = await ctx.storage.get('preferences');
        

        Constraints:

          Storage is per-plugin, per-user (plugins can't read other plugins' storage)

          Size limits enforced (e.g., 10MB per plugin)

          Syncs with user's Seed account (available across devices)

          Plugins cannot access browser localStorage, cookies, etc.

      8.5 UI Capabilities

        Plugins declare which UI surfaces they need:

        {
          "ui": {
            "panel": true,
            "modal": true,
            "contextMenu": true,
            "notification": true,
            "webView": false
          }
        }
        

        Why declare UI capabilities:

          Host can optimize (no webView = lighter weight)

          Security prompts can be more specific

          Users understand what the plugin does

        The webView: true capability triggers a specific warning (see section 14.3).

      8.6 Permission Prompts and User Consent

        When a plugin requests capabilities, Seed shows contextual prompts:

        At Install Time (basic capabilities):

        ┌──────────────────────────────────────────────────────────┐
        │  Install "Mermaid Diagrams"?                             │
        │                                                          │
        │  This plugin will be able to:                            │
        │  ✓ Render mermaid code blocks                            │
        │  ✓ Store preferences                                     │
        │                                                          │
        │  [Cancel]                              [Install Plugin]  │
        └──────────────────────────────────────────────────────────┘
        

        At First Use (sensitive capabilities):

        ┌──────────────────────────────────────────────────────────┐
        │  "AI Assistant" wants to access:                         │
        │                                                          │
        │  ⚠️  Read all content on this page                       │
        │  ⚠️  Connect to api.openai.com                           │
        │                                                          │
        │  This allows the plugin to send your document content    │
        │  to OpenAI's servers for processing.                     │
        │                                                          │
        │  [Deny]     [Allow Once]     [Always Allow]              │
        └──────────────────────────────────────────────────────────┘
        

        For Unrestricted UI (webView with network):

        ┌──────────────────────────────────────────────────────────┐
        │  ⚠️  "YouTube Embed" requests enhanced access            │
        │                                                          │
        │  This plugin includes components with full browser       │
        │  access. It can:                                         │
        │                                                          │
        │  • Connect to any website                                │
        │  • Use browser features (cookies, storage)               │
        │  • Track your activity within the plugin                 │
        │                                                          │
        │  Only install if you trust the developer.                │
        │                                                          │
        │  [Cancel]                              [I Understand]    │
        └──────────────────────────────────────────────────────────┘
        

      8.7 Capability Versioning

        When a plugin updates and requests new capabilities, users must re-consent:

        Version 1.0:

        { "capabilities": { "storage": true } }
        

        Version 2.0:

        { "capabilities": { "storage": true, "network": ["api.example.com"] } }
        

        On update, Seed shows:

        ┌──────────────────────────────────────────────────────────┐
        │  "Example Plugin" update requires new permissions        │
        │                                                          │
        │  New capabilities requested:                             │
        │  ⚠️  Connect to api.example.com                          │
        │                                                          │
        │  [Skip Update]           [Review & Update]               │
        └──────────────────────────────────────────────────────────┘
        

        This prevents plugins from acquiring capabilities through silent updates.

    9. Schema-First Block Design

      9.1 Why Schemas Are Mandatory

        Every block type (native or plugin-provided) must define a schema. This is not optional. Schemas enable:

        Validation at Write Time: Malformed data never persists. If a plugin tries to write invalid data, the host rejects it.

        Validation at Read Time: Plugins receive data guaranteed to match their schema. No defensive coding against malformed input.

        Portability: Seed understands block structure without executing plugin code. This enables:

          Rendering fallback UI for missing plugins

          Search indexing of block content

          Export to other formats

          Data migration when schemas evolve

        Type Safety: TypeScript SDK generates types from schemas. Plugin code has full IDE support.

      9.2 Schema Definition with JSON Schema or Zod

        Plugins can define schemas using JSON Schema or Zod (TypeScript):

        JSON Schema:

        {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "properties": {
            "videoId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{11}$" },
            "startTime": { "type": "number", "minimum": 0 },
            "autoplay": { "type": "boolean", "default": false }
          },
          "required": ["videoId"]
        }
        

        Zod (TypeScript):

        import { z } from 'zod';
        
        export const youtubeBlockSchema = z.object({
          videoId: z.string().regex(/^[A-Za-z0-9_-]{11}$/),
          startTime: z.number().min(0).optional(),
          autoplay: z.boolean().default(false),
        });
        
        export type YouTubeBlockData = z.infer<typeof youtubeBlockSchema>;
        

        The SDK converts Zod schemas to JSON Schema for the manifest. Plugin code uses the Zod types directly.

      9.3 Seed-Native Schema Primitives

        Plugins can reference Seed's built-in types:

        import { z } from 'zod';
        import { SeedSchemas } from '@seed/plugin-sdk';
        
        export const taskBlockSchema = z.object({
          title: z.string(),
          
          // Reference to another document
          linkedDoc: SeedSchemas.documentRef,
          
          // Rich text in Seed's native format
          description: SeedSchemas.richText,
          
          // Reference to a Seed user
          assignee: SeedSchemas.userRef,
          
          // Timestamp in Seed's format
          dueDate: SeedSchemas.timestamp,
          
          // Reference to an uploaded file
          attachment: SeedSchemas.fileRef.optional(),
        });
        

        Why native primitives matter:

          Resolution: Seed resolves refs correctly (permissions, sync, link previews)

          Rich text: Plugin doesn't reinvent text formatting

          Consistency: Users, dates, files work the same everywhere

          Indexing: Seed can index relationships and text content

      9.4 Schema Validation Flow

        Plugin requests document write
                 │
                 ▼
            ┌────────────────────────────┐
            │   Host validates schema    │
            │                            │
            │   1. Check required fields │
            │   2. Validate types        │
            │   3. Run custom validators │
            │   4. Check constraints     │
            └────────────────────────────┘
                 │
                 ├──► Invalid: Reject write, return error to plugin
                 │
                 └──► Valid: Apply through sync layer
                              │
                              ▼
                         Persisted to network
                              │
                              ▼
                         Replicated to collaborators
        

        When loading blocks:

        Block data retrieved from network
                 │
                 ▼
            ┌────────────────────────────┐
            │   Host validates schema    │
            │   (same checks)            │
            └────────────────────────────┘
                 │
                 ├──► Invalid: Render with warning, offer repair
                 │
                 └──► Valid: Pass to plugin.onRender()
        

      9.5 Schema Evolution and Migrations

        Schemas evolve. Plugins must handle this gracefully.

        Additive Changes (safe):

        // v1
        z.object({ videoId: z.string() });
        
        // v2 - adds optional field
        z.object({ 
          videoId: z.string(),
          startTime: z.number().optional(),  // New, optional
        });
        

        Old data remains valid. Plugin handles missing startTime.

        Breaking Changes (require migration):

        // v1
        z.object({ videoId: z.string() });
        
        // v2 - renames field
        z.object({ videoUrl: z.string() });  // Breaking!
        

        Plugins declare migrations:

        export const migrations = {
          '1→2': (data: V1Data): V2Data => ({
            videoUrl: `https://youtube.com/watch?v=${data.videoId}`,
          }),
        };
        

        Seed runs migrations before passing data to the plugin:

          Detect block's schema version

          Load plugin's migration chain

          Apply migrations sequentially

          Pass migrated data to plugin

      9.6 Graceful Degradation for Missing Plugins

        When User A creates a block with Plugin X, and User B opens the document without Plugin X:

        ┌─────────────────────────────────────────────────────────┐
        │  ⚠️ YouTube Embed                                       │
        │                                                         │
        │  This block requires the "YouTube" plugin to display.   │
        │                                                         │
        │  Block data:                                            │
        │  • videoId: "dQw4w9WgXcQ"                               │
        │  • startTime: 42                                        │
        │                                                         │
        │  [Install Plugin]    [Show as JSON]                     │
        └─────────────────────────────────────────────────────────┘
        

        Because Seed has the schema, it can:

          Show structured data (not raw JSON dump)

          Offer to install the missing plugin

          Allow editing raw data (with schema validation)

          Preserve the block through edits (no data loss)

    10. The Extension Point Model

      10.1 Native Blocks as Foundation

        Seed provides robust native blocks that handle common use cases without plugins:

          Text: Paragraphs, headings, lists

          Code: Syntax-highlighted code with language selection

          Image: Uploaded or linked images

          Video: Embedded video from URLs (handles YouTube, Vimeo, etc. natively)

          Embed: Generic URL embeds (oEmbed)

          Table: Data tables with sorting/filtering

          File: File attachments

          Divider: Visual separators

          Callout: Highlighted information boxes

        Critical design principle: Native blocks should handle as much as possible. For example, the native video block already handles YouTube URLs — it doesn't require a "YouTube plugin" to function. Plugins enhance the native experience (custom player controls, chapter markers, etc.) rather than providing basic functionality.

        These native blocks work for all users without plugins. They're the foundation that ensures content is always accessible.

      10.2 Extension Points for Native Blocks

        Rather than creating "mermaid-block" or "youtube-block" as entirely new block types, plugins extend native blocks:

        Code Block Extension:

        {
          "surfaces": {
            "mermaidRenderer": {
              "type": "block",
              "extends": "code",
              "when": { "language": "mermaid" },
              "entry": "blocks/mermaid.wasm"
            }
          }
        }
        

        This says: "When a code block has language: mermaid, I can render it."

        Video Block Extension:

        {
          "surfaces": {
            "youtubeEnhanced": {
              "type": "block",
              "extends": "video",
              "when": { "urlPattern": "youtube.com|youtu.be" },
              "entry": "blocks/youtube.wasm"
            }
          }
        }
        

        This says: "When a video block's URL matches YouTube, I can provide enhanced rendering."

        Benefits:

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

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

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

          Discoverable: Users see "Install plugin for enhanced rendering" rather than broken content

      10.3 The "Open With" Paradigm

        When multiple plugins can handle a block, Seed presents a choice similar to operating system file associations:

        ┌─────────────────────────────────────────────────────────┐
        │  How would you like to render this code block?          │
        │                                                         │
        │  Language: mermaid                                      │
        │                                                         │
        │  ○ Seed (syntax highlighting only)                      │
        │  ◉ Mermaid Diagrams (render as diagram)                 │
        │  ○ Mermaid Pro (render with themes)                     │
        │                                                         │
        │  ☐ Remember this choice for mermaid blocks              │
        │                                                         │
        │  [Cancel]                            [Use Selection]    │
        └─────────────────────────────────────────────────────────┘
        

        User preferences stored:

        {
          "blockHandlers": {
            "code:mermaid": "plugin:mermaid-diagrams",
            "code:graphviz": "plugin:graphviz-renderer",
            "video:youtube.com": "plugin:youtube-enhanced"
          }
        }
        

      10.4 Fallback Hierarchy

        When rendering a block, Seed follows this priority:

          User's chosen plugin (if set)

          Most capable installed plugin (by plugin-declared priority)

          Native Seed renderer (always available)

        If a plugin crashes or times out, Seed falls back to native rendering with a warning:

        ┌─────────────────────────────────────────────────────────┐
        │  ⚠️ Plugin renderer failed                              │
        │                                                         │
        │  ┌─────────────────────────────────────────────────┐   │
        │  │ ```mermaid                                       │   │
        │  │ graph TD                                        │   │
        │  │   A-->B                                         │   │
        │  │ ```                                             │   │
        │  └─────────────────────────────────────────────────┘   │
        │                                                         │
        │  [Retry]  [Report Issue]  [Use Seed Renderer]          │
        └─────────────────────────────────────────────────────────┘
        

    11. Plugin-to-Plugin Communication

      Plugins can expose services and call other plugins' services — but only through host mediation.

      Exposing a Service:

      // ai-service plugin
      export const aiService = defineService({
        name: 'ai-completion',
        
        methods: {
          complete: {
            input: z.object({ prompt: z.string(), maxTokens: z.number() }),
            output: z.object({ text: z.string() }),
            
            async handler(input, ctx) {
              const response = await ctx.network.fetch('https://api.openai.com/...', {
                method: 'POST',
                body: JSON.stringify({ prompt: input.prompt }),
              });
              return { text: response.choices[0].text };
            },
          },
        },
      });
      

      Calling Another Plugin's Service:

      // writing-assistant plugin
      async function enhanceText(ctx: PluginContext, text: string) {
        // Host mediates this call
        const result = await ctx.plugins.call('ai-completion', 'complete', {
          prompt: `Improve this text: ${text}`,
          maxTokens: 500,
        });
        
        return result.text;
      }
      

      Host Mediation:

      Plugin A calls ctx.plugins.call('service-b', 'method', data)
               │
               ▼
          ┌────────────────────────────────────────────────────┐
          │   Host validates:                                   │
          │   • Is Plugin A allowed to call Plugin B?           │
          │   • Does 'method' exist on service-b?               │
          │   • Does 'data' match method's input schema?        │
          │   • Rate limiting                                   │
          └────────────────────────────────────────────────────┘
               │
               ├──► Denied: Error returned to Plugin A
               │
               └──► Approved: Route to Plugin B's Wasm instance
                              │
                              ▼
                         Plugin B executes method
                              │
                              ▼
                         Response validated against output schema
                              │
                              ▼
                         Returned to Plugin A
      

      Why Mediation:

        Dependency tracking: Host knows which plugins depend on which

        Failure isolation: Plugin B crashing doesn't take down Plugin A

        Security: Plugin A can't access Plugin B's internals

        Billing/quotas: Host can track API usage per plugin

    12. Collaboration and Sync Integration

      Seed is a real-time collaborative platform. Plugins must integrate correctly with the sync layer.

      12.1 State Update Events

        When block data changes (from any source — local edit, collaborator edit, sync resolution), the plugin receives an event:

        export const diagramBlock = defineBlock({
          // ...
          
          async onStateUpdate(ctx, previousData, newData, source) {
            // source: 'local' | 'remote' | 'migration'
            
            if (source === 'remote') {
              // Collaborator made a change
              // Re-render with new data
              return this.onRender(ctx);
            }
            
            // Local changes already reflected
            return null;  // No UI update needed
          },
        });
        

        The host calls onStateUpdate whenever block data changes, regardless of source. Plugins don't need to poll or subscribe — the host pushes updates.

      12.2 Transactional Document Writes

        Plugins don't directly mutate document state. They request writes:

        async function addItem(ctx: PluginContext, item: string) {
          // Request a write
          const result = await ctx.document.updateBlock(ctx.blockId, (data) => ({
            ...data,
            items: [...data.items, item],
          }));
          
          if (result.conflict) {
            // Another edit happened simultaneously
            // result.resolved contains the merged state
            // Plugin can accept or retry
          }
        }
        

        Write Flow:

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

          Host applies transform to current state

          Host validates result against schema

          Host submits to CRDT sync layer

          Sync layer handles conflicts, ordering, persistence

          Host notifies plugin of result (success, conflict, resolution)

      12.3 Offline Behavior

        Seed works offline. Plugins must handle:

        Offline Writes:

        const result = await ctx.document.updateBlock(id, transform);
        
        if (result.status === 'queued') {
          // Write accepted locally, will sync when online
          // UI should reflect the local state
        }
        

        Network Requests Offline:

        try {
          const data = await ctx.network.fetch(url);
        } catch (e) {
          if (e.code === 'OFFLINE') {
            // Show cached data or offline message
            return ui.text('Content unavailable offline');
          }
        }
        

        Sync Conflicts:

        When the user comes back online, edits may conflict with remote changes. Seed's CRDT layer resolves most conflicts automatically. If manual resolution is needed, Seed handles the UI — plugins receive the resolved state via onStateUpdate.

    13. Platform Considerations

      13.1 Electron vs Web Parity

        Seed runs as both an Electron desktop app and a web application. Plugins should work identically on both platforms.

        Identical Behavior:

        | Capability | Web | Electron | |------------|-----|----------| | Wasm execution | ✅ Web Worker | ✅ Web Worker (renderer) | | Declarative UI | ✅ | ✅ | | webView (iframe) | ✅ | ✅ | | Network (permitted) | ✅ | ✅ | | Storage | ✅ | ✅ (synced) | | Document access | ✅ | ✅ | | Plugin-to-plugin | ✅ | ✅ |

        Electron-Only (explicitly unavailable on web):

        | Capability | Web | Electron | |------------|-----|----------| | Filesystem access | ❌ | ❌ (intentionally disabled) | | System notifications | Limited | ❌ (intentionally disabled) | | Native menus | ❌ | ❌ (use Seed's UI) | | Shell integration | ❌ | ❌ (security risk) |

        We intentionally disable Electron-specific capabilities to maintain parity. Plugins cannot rely on running in Electron.

      13.2 Worker Spawning Strategy

        For UI-centric surfaces (blocks, pages):

          Workers spawn in the browser/renderer process

          Direct message passing to main thread

          UI updates are low-latency

        For service surfaces:

          On web: Web Worker in browser

          On Electron: Web Worker in renderer (same as UI surfaces)

            Not Node.js Worker Threads, to maintain parity

        Why not use Node.js for Electron services:

        Node.js Worker Threads have access to Node APIs (filesystem, native modules). Using them would break parity with web and create security inconsistencies. By keeping all plugin code in browser-like workers, we maintain uniform sandboxing.

      13.3 Unsupported Capabilities

        To maintain security and parity, these capabilities are explicitly not supported:

          Filesystem access: Too dangerous, breaks web parity

          Native system notifications: Inconsistent cross-platform, use Seed's notification system

          Clipboard write without permission: Always requires user gesture

          Background execution when app hidden: Web browsers throttle this; we don't fight it

          Native dialogs: Use Seed's modal system

          Window/process spawning: Security risk

        Plugins that need these capabilities are outside Seed's plugin model. They could potentially be implemented as separate applications communicating via Seed's API.

    14. Security Model

      14.1 Wasm Sandbox Guarantees

        WebAssembly provides strong isolation guarantees:

        Memory Safety:

          Wasm linear memory is separate from host memory

          Plugins cannot read/write arbitrary host memory

          Buffer overflows stay within Wasm's memory space

          No pointer arithmetic into host space

        Execution Isolation:

          Wasm code cannot call arbitrary host functions

          Only explicitly imported functions are available

          Stack is protected (separate from linear memory)

        No Implicit Capabilities:

          No filesystem access unless injected

          No network access unless injected

          No DOM access (Wasm can't see the browser)

          No environment variables, system info

        Deterministic Execution:

          Same inputs produce same outputs

          No access to system time (unless injected)

          No random numbers (unless injected)

          Reproducible behavior

      14.2 iframe Trust Boundaries

        iframes (webView components) have different trust characteristics:

        iframe CAN:

          Access browser APIs (DOM, Canvas, WebGL)

          Make fetch requests (subject to CORS)

          Use localStorage (scoped to plugin origin)

          Run arbitrary JavaScript

        iframe CANNOT:

          Access Seed's DOM (null origin, cross-origin isolation)

          Access document data (never passed to iframe directly)

          Read other plugins' storage

          Escape the iframe boundary

        Residual Risk:

        If Wasm sends sensitive document data to an iframe for visualization, the iframe JavaScript could exfiltrate it via fetch. Mitigations:

          CSP restrictions: Limit iframe's fetch destinations

          Data minimization: Send only necessary data to iframe

          User warnings: Clearly indicate plugins with iframe access

      14.3 The "Unrestricted" UI Tier

        Plugins declare their rendering mode:

        {
          "surfaces": {
            "basicBlock": {
              "type": "block",
              "render": "sandboxed"  // Declarative UI only
            },
            "complexBlock": {
              "type": "block",
              "render": "unrestricted"  // Can use webView with network
            }
          }
        }
        

        "sandboxed" render mode:

          Declarative UI components only

          webView prohibited

          Maximum security, minimal warnings

        "unrestricted" render mode:

          Can use webView component

          iframe has browser APIs

          Triggers security warning at install

        The prompt for unrestricted UI clearly communicates risks:

        ⚠️ This plugin requests enhanced UI access
        
        It can:
        • Display custom web content
        • Connect to external websites
        • Use cookies and browser storage
        
        Your document content may be visible to this plugin's
        UI components. Only install if you trust the developer.
        

      14.4 Attack Surface Analysis

        Threat: Malicious plugin exfiltrates document data

        Mitigations:

          Wasm sandbox: Plugin can't access network without permission

          Network whitelisting: Can only contact declared domains

          User consent: User approves network access

          Audit log: Host logs all network requests

        Residual risk: User approves network to attacker's domain. Mitigation: Domain reputation checking, warnings for new/unknown domains.

        Threat: Plugin crashes repeatedly, degrading experience

        Mitigations:

          Crash isolation: Worker restarts don't affect main app

          Crash counting: After N crashes, plugin disabled

          Timeouts: Long-running operations terminated

          Fallback: Native rendering takes over

        Threat: Plugin consumes excessive resources

        Mitigations:

          Memory limits: Wasm instance has memory cap

          CPU limits: Execution time limits per operation

          Storage quotas: Per-plugin storage limits

          Rate limiting: Network requests, document writes rate-limited

        Threat: Plugin performs clickjacking via iframe

        Mitigations:

          iframe sandboxed with restrictive attributes

          Parent can't be navigated from iframe

          UI rendered within Seed's frame, not overlayed

        Threat: Plugin supply chain attack (compromised update)

        Mitigations:

          Capability versioning: New capabilities require re-consent

          Update review: Changed capabilities highlighted to user

          Rollback: Users can revert to previous plugin version

          Signing: Plugins signed by developer key

    15. SDK Design

      15.1 TypeScript-First Approach

        The primary SDK is TypeScript, optimized for developer experience:

        import { 
          defineBlock, 
          defineAction, 
          defineService,
          ui,
          z,
        } from '@seed/plugin-sdk';
        
        // Full type inference from schemas
        const schema = z.object({
          title: z.string(),
          count: z.number().min(0),
        });
        
        export const counterBlock = defineBlock({
          name: 'counter',
          schema,
          
          // ctx.blockData is typed as { title: string; count: number }
          async onRender(ctx) {
            return ui.container({ direction: 'column' }, [
              ui.heading(ctx.blockData.title, 2),
              ui.text(`Count: ${ctx.blockData.count}`),
              ui.button('Increment', { id: 'inc' }),
            ]);
          },
          
          async onEvent(ctx, event) {
            if (event.type === 'click' && event.id === 'inc') {
              await ctx.document.updateBlock(ctx.blockId, (data) => ({
                ...data,
                count: data.count + 1,  // Typed!
              }));
            }
          },
        });
        

        SDK Features:

          Type inference: Schemas generate TypeScript types automatically

          IDE support: Autocomplete for all APIs, UI components

          Validation: Build-time checking of manifest against code

          Code splitting: SDK handles Wasm/iframe bundling

      15.2 Lower-Level Language Support

        Developers can write plugins in any language compiling to Wasm:

        Rust:

        use seed_plugin_sdk::prelude::*;
        
        #[seed_block]
        pub struct CounterBlock {
            title: String,
            count: u32,
        }
        
        impl Block for CounterBlock {
            fn render(&self, ctx: &Context) -> UI {
                ui::container(Direction::Column, vec![
                    ui::heading(&self.title, 2),
                    ui::text(&format!("Count: {}", self.count)),
                    ui::button("Increment").id("inc"),
                ])
            }
            
            fn on_event(&mut self, ctx: &mut Context, event: Event) {
                if event.is_click("inc") {
                    self.count += 1;
                    ctx.update_block(self);
                }
            }
        }
        

        Go:

        package main
        
        import (
            sdk "github.com/seed/plugin-sdk-go"
        )
        
        type CounterBlock struct {
            Title string `json:"title"`
            Count int    `json:"count"`
        }
        
        func (b *CounterBlock) Render(ctx *sdk.Context) sdk.UI {
            return sdk.Container(sdk.Column,
                sdk.Heading(b.Title, 2),
                sdk.Text(fmt.Sprintf("Count: %d", b.Count)),
                sdk.Button("Increment").ID("inc"),
            )
        }
        
        func (b *CounterBlock) OnEvent(ctx *sdk.Context, event sdk.Event) {
            if event.IsClick("inc") {
                b.Count++
                ctx.UpdateBlock(b)
            }
        }
        

        Lower-level SDKs provide the same capabilities with less automatic magic. Developers handle more serialization/deserialization themselves.

      15.3 SDK Component Library

        The UI components available in all SDKs:

        namespace ui {
          // Layout
          function container(props: ContainerProps, children: Component[]): Component;
          function divider(): Component;
          function spacer(size: 'small' | 'medium' | 'large'): Component;
          
          // Typography
          function text(content: string, props?: TextProps): Component;
          function heading(content: string, level: 1 | 2 | 3): Component;
          
          // Form Controls
          function textInput(props: TextInputProps): Component;
          function textArea(props: TextAreaProps): Component;
          function select(props: SelectProps): Component;
          function checkbox(props: CheckboxProps): Component;
          function toggle(props: ToggleProps): Component;
          function slider(props: SliderProps): Component;
          
          // Actions
          function button(label: string, props?: ButtonProps): Component;
          function buttonGroup(buttons: ButtonProps[]): Component;
          
          // Display
          function image(props: ImageProps): Component;
          function icon(name: string, props?: IconProps): Component;
          function badge(content: string, props?: BadgeProps): Component;
          function progressBar(value: number, max: number): Component;
          
          // Structure
          function list(props: ListProps): Component;
          function tabs(props: TabsProps): Component;
          function accordion(props: AccordionProps): Component;
          
          // Escape Hatch
          function webView(props: WebViewProps): Component;
        }
        

      15.4 Build Toolchain

        The SDK includes a build toolchain:

        # Initialize a new plugin project
        npx @seed/create-plugin my-plugin
        
        # Development with hot reload
        npm run dev
        
        # Build for production
        npm run build
        
        # Outputs:
        # - dist/manifest.json
        # - dist/plugin.wasm
        # - dist/ui/*.js (iframe bundles, if any)
        # - dist/schemas/*.json
        

        Build Steps:

          TypeScript compilation: Type-check all code

          Schema extraction: Generate JSON Schemas from Zod definitions

          Code splitting: Separate Wasm code from iframe code

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

          Bundle optimization: Tree-shake, minify iframe bundles

          Manifest generation: Produce final manifest.json

          Validation: Check manifest against built artifacts

    16. Wasm Instance Management

      16.1 One Instance Per Plugin

        Seed creates one Wasm instance per installed plugin, not per block or per surface. This balances memory usage with isolation:

        Why not one instance per block?

          50 diagram blocks = 50 Wasm instances = significant memory overhead

          Startup time multiplied by block count

          Excessive for common cases

        Why not shared instances across plugins?

          Plugins could interfere with each other

          Crash in one plugin affects others

          No capability isolation between plugins

        One instance per plugin:

          Reasonable memory footprint

          Plugin-level isolation maintained

          Single cold start per plugin per session

      16.2 Block Instance Multiplexing

        When a plugin renders multiple blocks, the host multiplexes through the single Wasm instance:

        ┌─────────────────────────────────────────────────────┐
        │                                                     │
        │  Document with 5 diagram blocks                     │
        │                                                     │
        │  Block A ──┐                                        │
        │  Block B ──┤                                        │
        │  Block C ──┼──► Single Wasm Instance ──► Renders    │
        │  Block D ──┤    (diagram plugin)                    │
        │  Block E ──┘                                        │
        │                                                     │
        └─────────────────────────────────────────────────────┘
        

        Host responsibilities:

          Track which blocks are rendered by which plugin

          Route events to correct handlers with block ID

          Pass correct block data for each render call

          Handle concurrent requests (queue or parallelize)

        Plugin responsibilities:

          Handle onRender(ctx) where ctx.blockId identifies which block

          Maintain internal state keyed by block ID (if needed)

          Clean up when onUnmount(blockId) called

      16.3 Memory and Lifecycle

        Memory Limits:

        Each Wasm instance has a memory cap (e.g., 256MB). If a plugin exceeds this:

          Wasm traps (controlled crash)

          Host receives error

          Host disables plugin temporarily

          User notified with option to restart or uninstall

        Instance Lifecycle:

        Plugin installed
                 │
                 ▼
            Instance created (lazy, on first use)
                 │
                 ├──► Block rendered ──► Instance kept alive
                 │
                 ├──► Action executed ──► Instance kept alive
                 │
                 ├──► Idle timeout (5 min) ──► Instance terminated
                 │
                 └──► Plugin disabled/uninstalled ──► Instance terminated
        

        Instances are created lazily (first use) and terminated after idle periods to conserve memory.

    17. Timing and Loading

      17.1 The Cold Start Problem

        When a user opens a document with plugin-rendered blocks, the plugins must load before rendering:

        User opens document
                 │
                 ▼
            Document loaded, blocks identified
                 │
                 ├──► Native blocks: Render immediately
                 │
                 └──► Plugin blocks: 
                           │
                           ▼
                      Load plugin Wasm (50-200ms)
                           │
                           ▼
                      Call onRender() (10-50ms)
                           │
                           ▼
                      Render UI
        

        This delay is visible to users. We mitigate it with loading states and schema defaults.

      17.2 Schema Defaults and Skeletons

        Schema-Based Skeleton:

        While the plugin loads, Seed renders a skeleton based on the schema:

        const schema = z.object({
          title: z.string(),
          items: z.array(z.string()),
          chartType: z.enum(['bar', 'line', 'pie']),
        });
        

        Seed can render:

        ┌─────────────────────────────────────────┐
        │  ████████████████  (title placeholder)  │
        │                                         │
        │  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  │
        │  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  │
        │  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  │
        │            (items placeholder)          │
        │                                         │
        └─────────────────────────────────────────┘
        

        Plugin-Declared Skeleton:

        Plugins can provide custom loading UI:

        export const chartBlock = defineBlock({
          schema,
          
          loadingUI: ui.container({ direction: 'column' }, [
            ui.skeleton({ type: 'text', width: '60%' }),
            ui.skeleton({ type: 'chart', height: 200 }),
          ]),
          
          // ...
        });
        

      17.3 Lazy Loading Strategy

        Viewport-Based Loading:

        Only load plugins for blocks currently in (or near) the viewport:

        ┌──────────────────────────────────────────────────┐
        │                                                  │
        │  ┌────────────────────┐  ◄── In viewport:        │
        │  │   Plugin Block A   │      Plugin loaded       │
        │  └────────────────────┘                          │
        │                                                  │
        │  ┌────────────────────┐  ◄── Near viewport:      │
        │  │   Plugin Block B   │      Plugin preloading   │
        │  └────────────────────┘                          │
        │                                                  │
        ├──────────────────────────────────────────────────┤
        │                                                  │
        │  ┌────────────────────┐  ◄── Far from viewport:  │
        │  │   Plugin Block C   │      Not loaded yet      │
        │  └────────────────────┘                          │
        │                                                  │
        │  ┌────────────────────┐                          │
        │  │   Plugin Block D   │      Not loaded yet      │
        │  └────────────────────┘                          │
        │                                                  │
        └──────────────────────────────────────────────────┘
        

        Preloading Heuristics:

          Preload plugins for blocks near the scroll position

          Preload plugins the user has recently interacted with

          Preload plugins on mouse hover (anticipate interaction)

    18. Plugin Manifest Specification

      Complete manifest structure:

      {
        "$schema": "https://seed.dev/schemas/plugin-manifest-v1.json",
        
        "id": "com.example.mermaid-diagrams",
        "name": "Mermaid Diagrams",
        "version": "1.2.0",
        "description": "Render Mermaid diagrams in code blocks",
        "author": {
          "name": "Example Inc",
          "email": "plugins@example.com",
          "url": "https://example.com"
        },
        "license": "MIT",
        "repository": "https://github.com/example/seed-mermaid",
        
        "seed": {
          "minVersion": "2.0.0",
          "maxVersion": "3.x"
        },
        
        "capabilities": {
          "network": [],
          "storage": true,
          "document": {
            "read": "current-block",
            "write": "current-block"
          }
        },
        
        "surfaces": {
          "mermaidBlock": {
            "type": "block",
            "extends": "code",
            "when": { "language": "mermaid" },
            "schema": "./schemas/mermaid.json",
            "entry": "./dist/blocks/mermaid.wasm",
            "render": "sandboxed",
            "priority": 100
          }
        },
        
        "migrations": {
          "1.0.0→1.1.0": "./migrations/1.0-to-1.1.js",
          "1.1.0→1.2.0": "./migrations/1.1-to-1.2.js"
        },
        
        "assets": [
          "assets/icons/*.svg"
        ]
      }
      

      Fields:

      | Field | Required | Description | |-------|----------|-------------| | id | Yes | Unique identifier (reverse domain) | | name | Yes | Display name | | version | Yes | SemVer version | | description | Yes | Short description | | author | Yes | Author information | | license | Yes | SPDX license identifier | | seed.minVersion | Yes | Minimum Seed version | | capabilities | Yes | Required permissions | | surfaces | Yes | Extension points | | migrations | No | Schema migration scripts | | assets | No | Additional files to include |

    19. Detailed Examples

      19.1 Example: Mermaid Diagram Block Extension

        A plugin that renders Mermaid syntax in code blocks as diagrams.

        manifest.json:

        {
          "id": "com.mermaid.seed-plugin",
          "name": "Mermaid Diagrams",
          "version": "1.0.0",
          "capabilities": {
            "storage": true,
            "document": { "read": "current-block", "write": "current-block" }
          },
          "surfaces": {
            "mermaidRenderer": {
              "type": "block",
              "extends": "code",
              "when": { "language": "mermaid" },
              "schema": "./schemas/mermaid.json",
              "entry": "./dist/mermaid.wasm",
              "render": "sandboxed"
            }
          }
        }
        

        schemas/mermaid.json:

        {
          "type": "object",
          "properties": {
            "code": { "type": "string" },
            "language": { "const": "mermaid" },
            "theme": { "enum": ["default", "dark", "forest", "neutral"], "default": "default" }
          },
          "required": ["code", "language"]
        }
        

        src/mermaid-block.ts:

        import { defineBlock, ui, webView, z } from '@seed/plugin-sdk';
        import mermaid from 'mermaid';
        
        const schema = z.object({
          code: z.string(),
          language: z.literal('mermaid'),
          theme: z.enum(['default', 'dark', 'forest', 'neutral']).default('default'),
        });
        
        export const mermaidBlock = defineBlock({
          name: 'mermaid-renderer',
          schema,
          
          async onRender(ctx) {
            const { code, theme } = ctx.blockData;
            
            // Render mermaid to SVG in Wasm context
            mermaid.initialize({ theme });
            const { svg } = await mermaid.render('diagram', code);
            
            return ui.container({ direction: 'column' }, [
              // Show rendered diagram
              ui.container({ className: 'diagram-container' }, [
                ui.rawHtml(svg),  // SVG rendered by host
              ]),
              
              // Theme selector
              ui.select({
                id: 'theme',
                label: 'Theme',
                value: theme,
                options: [
                  { value: 'default', label: 'Default' },
                  { value: 'dark', label: 'Dark' },
                  { value: 'forest', label: 'Forest' },
                  { value: 'neutral', label: 'Neutral' },
                ],
              }),
              
              // Toggle to show source
              ui.toggle({ id: 'showSource', label: 'Show Source', enabled: false }),
            ]);
          },
          
          async onEvent(ctx, event) {
            if (event.type === 'change' && event.id === 'theme') {
              await ctx.document.updateBlock(ctx.blockId, (data) => ({
                ...data,
                theme: event.value,
              }));
            }
          },
        });
        

        User Experience:

          User creates a code block with language "mermaid"

          Without plugin: Seed shows syntax-highlighted mermaid code

          With plugin installed: Seed renders the diagram visually

          User can select themes, toggle source view

          If plugin crashes: Falls back to syntax highlighting

      19.2 Example: AI Writing Assistant Action

        An action that improves selected text using an AI service.

        manifest.json:

        {
          "id": "com.example.ai-writer",
          "name": "AI Writing Assistant",
          "version": "1.0.0",
          "capabilities": {
            "network": ["api.openai.com"],
            "document": { "read": "current-page", "write": "current-page" }
          },
          "surfaces": {
            "improveText": {
              "type": "action",
              "label": "Improve Writing",
              "description": "Use AI to improve the selected text",
              "icon": "magic-wand",
              "shortcut": "Cmd+Shift+I",
              "entry": "./dist/improve.wasm",
              "parameters": {
                "type": "object",
                "properties": {
                  "style": {
                    "type": "string",
                    "enum": ["professional", "casual", "academic"],
                    "default": "professional"
                  }
                }
              }
            }
          }
        }
        

        src/improve-action.ts:

        import { defineAction, ui, z } from '@seed/plugin-sdk';
        
        const paramsSchema = z.object({
          style: z.enum(['professional', 'casual', 'academic']).default('professional'),
        });
        
        export const improveAction = defineAction({
          name: 'improve-text',
          parameters: paramsSchema,
          
          async onExecute(ctx, params) {
            // Get selected text
            const selection = await ctx.document.getSelection();
            
            if (!selection || selection.text.length === 0) {
              ctx.toast('Please select some text first', 'warning');
              return;
            }
            
            // Show progress
            ctx.showProgress('Improving text...', 0);
            
            try {
              // Call OpenAI API (host-mediated)
              const response = await ctx.network.fetch('https://api.openai.com/v1/chat/completions', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                  model: 'gpt-4',
                  messages: [{
                    role: 'user',
                    content: `Improve this text to be more ${params.style}:\n\n${selection.text}`,
                  }],
                }),
              });
              
              ctx.showProgress('Improving text...', 50);
              
              const data = await response.json();
              const improvedText = data.choices[0].message.content;
              
              // Replace selection with improved text
              await ctx.document.replaceSelection(improvedText);
              
              ctx.showProgress('Done!', 100);
              ctx.toast('Text improved successfully', 'success');
              
            } catch (error) {
              ctx.toast(`Error: ${error.message}`, 'error');
            }
          },
        });
        

        User Experience:

          User selects text in document

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

          Parameter modal appears (if style not already chosen)

          Progress indicator shows while AI processes

          Selected text replaced with improved version

          Can undo with Cmd+Z (Seed's native undo)

      19.3 Example: Analytics Service

        A background service that tracks document analytics and exposes an API to other plugins.

        manifest.json:

        {
          "id": "com.example.analytics",
          "name": "Document Analytics",
          "version": "1.0.0",
          "capabilities": {
            "storage": true,
            "document": { "read": "workspace" }
          },
          "surfaces": {
            "analyticsService": {
              "type": "service",
              "provides": ["word-count", "reading-time", "complexity-score"],
              "entry": "./dist/analytics.wasm"
            },
            "analyticsPage": {
              "type": "page",
              "path": "/plugins/analytics",
              "label": "Analytics",
              "icon": "chart-bar",
              "navigation": "sidebar",
              "entry": "./dist/page.wasm"
            }
          }
        }
        

        src/analytics-service.ts:

        import { defineService, z } from '@seed/plugin-sdk';
        
        export const analyticsService = defineService({
          name: 'analytics',
          
          methods: {
            'word-count': {
              input: z.object({ documentId: z.string() }),
              output: z.object({ count: z.number() }),
              
              async handler(input, ctx) {
                const doc = await ctx.document.get(input.documentId);
                const text = extractText(doc);
                return { count: text.split(/\s+/).length };
              },
            },
            
            'reading-time': {
              input: z.object({ documentId: z.string(), wpm: z.number().default(200) }),
              output: z.object({ minutes: z.number() }),
              
              async handler(input, ctx) {
                const { count } = await this.methods['word-count'].handler(
                  { documentId: input.documentId }, 
                  ctx
                );
                return { minutes: Math.ceil(count / input.wpm) };
              },
            },
            
            'complexity-score': {
              input: z.object({ documentId: z.string() }),
              output: z.object({ score: z.number(), grade: z.string() }),
              
              async handler(input, ctx) {
                const doc = await ctx.document.get(input.documentId);
                const score = calculateFleschKincaid(doc);
                const grade = scoreToGradeLevel(score);
                return { score, grade };
              },
            },
          },
        });
        

        Other plugins can call this service:

        // In another plugin
        const analytics = await ctx.plugins.call('analytics', 'reading-time', {
          documentId: ctx.documentId,
          wpm: 250,
        });
        
        console.log(`Reading time: ${analytics.minutes} minutes`);
        

      19.4 Example: Full-Featured Plugin with Multiple Surfaces

        A comprehensive task management plugin demonstrating multiple surfaces working together.

        manifest.json:

        {
          "id": "com.example.tasks",
          "name": "Task Manager",
          "version": "2.0.0",
          "capabilities": {
            "network": ["api.example.com"],
            "storage": true,
            "document": { "read": "workspace", "write": "workspace" }
          },
          "surfaces": {
            "taskBlock": {
              "type": "block",
              "blockType": "task",
              "schema": "./schemas/task.json",
              "entry": "./dist/blocks/task.wasm",
              "render": "sandboxed"
            },
            "taskListBlock": {
              "type": "block",
              "blockType": "task-list",
              "schema": "./schemas/task-list.json",
              "entry": "./dist/blocks/task-list.wasm",
              "render": "sandboxed"
            },
            "createTask": {
              "type": "action",
              "label": "Create Task",
              "icon": "plus-circle",
              "shortcut": "Cmd+Shift+T",
              "entry": "./dist/actions/create.wasm"
            },
            "taskService": {
              "type": "service",
              "provides": ["task-sync"],
              "entry": "./dist/services/sync.wasm"
            },
            "taskDashboard": {
              "type": "page",
              "path": "/plugins/tasks",
              "label": "Tasks",
              "icon": "check-square",
              "navigation": "sidebar",
              "entry": "./dist/pages/dashboard.wasm"
            }
          }
        }
        

        This plugin provides:

          Task block: Inline task items in documents

          Task list block: Aggregated task views

          Create task action: Quick task creation from anywhere

          Task service: Background sync with external task system

          Task dashboard: Full-page task management view

        All surfaces share the same Wasm instance and can communicate through internal state.

    20. Comparison to Existing Systems

      | Aspect | Seed | VS Code | Figma | WordPress | |--------|------|---------|-------|-----------| | Sandboxing | Wasm (strong) | None | Wasm (strong) | None | | UI Model | Declarative + iframe | Webview | iframe only | PHP templates | | Language | Any → Wasm | JS/TS | JS/TS | PHP | | Data Schema | Required | Optional | N/A | Optional | | Fallback | Always (native blocks) | N/A | N/A | Partial | | Permissions | Capability-based | None | Limited | None | | Platform | Web + Electron | Electron | Web | Server |

      Seed's Advantages:

        Security without sacrifice: Strong sandboxing without losing flexibility

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

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

        Extension points: Enhance native blocks rather than fragmenting the ecosystem

        True cross-platform: Same plugins work web and desktop

      Tradeoffs We Accept:

        Message passing overhead: All plugin communication goes through the host

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

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

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

    21. Open Questions and Future Considerations

      Questions to Resolve

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

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

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

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

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

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

      Future Considerations

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

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

        Plugin Composition: Can plugins extend other plugins' surfaces?

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

        Plugin Metrics: What telemetry should plugins have access to?

        Deprecation Policy: How do we retire old plugin APIs?

    22. Conclusion

      Seed's plugin architecture represents a synthesis of lessons from across the software industry. By combining WebAssembly's security guarantees with a declarative UI layer, schema-first data design, and the extension point model, we achieve something genuinely new:

      Plugins that are:

        Secure by default (Wasm sandbox, capability-based permissions)

        Beautiful by default (declarative UI, native rendering)

        Portable by default (schemas, graceful degradation)

        Discoverable by default (extension points, "open with" paradigm)

      For developers:

        TypeScript-first SDK with excellent ergonomics

        Escape hatches for complex cases (lower-level languages, iframe UI)

        Clear capability model (no guessing what's allowed)

        Schema-driven development (types, validation, migration)

      For users:

        Plugins enhance, not fragment, the experience

        Content remains accessible without plugins installed

        Clear, contextual permission prompts

        Consistent, native-feeling UI

      This architecture positions Seed to have a thriving plugin ecosystem while maintaining the security, consistency, and reliability that collaborative document editing demands.

      Document authored January 2026. Architecture subject to refinement based on implementation experience and community feedback.