@voiden/sdk

May 14, 2026 ยท View on GitHub

npm version License: MIT TypeScript CI

Official SDK for building Voiden extensions with support for both UI (renderer process) and Electron (main process).

What is Voiden?

Voiden is an extensible, privacy-focused editor built on Electron and Tiptap. The Voiden SDK enables developers to create powerful extensions that enhance the editor's functionality, from custom blocks and slash commands to system-level integrations.

Features

  • ๐ŸŽจ UI Extensions - Build custom editor blocks, slash commands, sidebars, and panels
  • โšก Electron Extensions - Access native APIs, file system, and system integrations
  • ๐Ÿ”„ Cross-Process Communication - Seamless IPC between UI and Electron extensions
  • ๐Ÿ’พ Storage API - Extension-scoped persistent storage
  • ๐Ÿ”Œ Pipeline System - Hook into request/response lifecycle
  • ๐ŸŒ Environment API - Secure access to environment variables
  • ๐Ÿ“ฆ Helper System - Cross-extension communication and shared functionality
  • ๐Ÿ”’ Type-Safe - Full TypeScript support with complete type definitions

Prerequisites

  • Node.js 20.x or higher
  • npm 10.x or higher (or yarn/pnpm)
  • TypeScript 5.x knowledge recommended
  • React 18.x (for UI extensions)
  • Tiptap 2.x (for custom editor blocks)

Installation

npm install @voiden/sdk
# or
yarn add @voiden/sdk

Installing Peer Dependencies

For UI extensions, you'll also need to install the required peer dependencies:

npm install react react-dom @tiptap/core @tiptap/pm @tiptap/react

Quick Start

Create your first extension in minutes:

import { UIExtension } from '@voiden/sdk/ui';

export class MyExtension extends UIExtension {
  name = 'my-first-extension';
  version = '1.0.0';
  description = 'My first Voiden extension';

  async onLoad() {
    this.registerSlashCommand({
      name: 'greet',
      label: 'Say Hello',
      slash: '/hello',
      description: 'Insert a greeting',
      action: (editor) => {
        editor.commands.insertContent('Hello from my extension!');
      },
    });
  }
}

Architecture

The SDK is divided into three parts:

  • UI Extensions (@voiden/sdk/ui) - Run in the renderer process, handle editor blocks, UI components
  • Electron Extensions (@voiden/sdk/electron) - Run in the main process, handle IPC, file system, native APIs
  • Shared (@voiden/sdk/shared) - Shared types and utilities

UI Extension Example

import { UIExtension } from '@voiden/sdk/ui';
import { MyCustomNode } from './nodes/MyCustomNode';

export class MyUIExtension extends UIExtension {
  name = 'my-ui-extension';
  version = '1.0.0';
  description = 'My awesome UI extension';

  async onLoad() {
    // Register a custom block
    this.registerBlock({
      name: 'my-custom-block',
      label: 'My Custom Block',
      node: MyCustomNode,
      icon: 'box',
    });

    // Register a slash command
    this.registerSlashCommand({
      name: 'insert-custom',
      label: 'Custom Block',
      slash: '/custom',
      description: 'Insert a custom block',
      action: (editor) => {
        editor.commands.insertContent({ type: 'my-custom-block' });
      },
    });

    // Register a sidebar
    this.registerSidebar('right', {
      id: 'my-sidebar',
      title: 'My Sidebar',
      component: MySidebarComponent,
    });
  }
}

Electron Extension Example

import { ElectronExtension } from '@voiden/sdk/electron';

export class MyElectronExtension extends ElectronExtension {
  name = 'my-electron-extension';
  version = '1.0.0';
  description = 'My awesome Electron extension';

  async onLoad() {
    // Register IPC handler
    this.registerIPCHandler('my-channel', async (data) => {
      const result = await this.processData(data);
      return { success: true, result };
    });

    // Register custom protocol
    this.registerProtocol({
      scheme: 'myapp',
      handler: async (url) => {
        return { data: 'Custom protocol response' };
      },
    });

    // Watch file system
    this.watchFileSystem({
      path: '/path/to/watch',
      onChange: (event, path) => {
      },
    });
  }

  private async processData(data: any) {
    // Custom processing logic
    return data;
  }
}

Full Extension (UI + Electron)

Many extensions need both UI and Electron components:

// index.ts
import { MyUIExtension } from './ui';
import { MyElectronExtension } from './electron';

export const ui = new MyUIExtension();
export const electron = new MyElectronExtension();

API Reference

UI Extension APIs

Core Methods

  • registerBlock(block: BlockDefinition) - Register a Tiptap node as a custom block
  • registerSlashCommand(command: SlashCommandDefinition) - Register a slash command
  • registerSlashGroup(group: SlashCommandGroup) - Register a group of slash commands
  • registerSidebar(side: 'left' | 'right', tab: TabDefinition) - Register a sidebar tab
  • registerPanel(panel: PanelDefinition) - Register a bottom panel
  • showModal(modal: ModalDefinition) - Show a modal dialog
  • showToast(message: string, type?: 'info' | 'success' | 'error') - Show a toast notification
  • getEditor(type?: 'main' | 'note') - Get the active editor instance

Advanced APIs

  • storage - Extension-scoped persistent storage
  • pipeline - Hook into request/response lifecycle
  • environment - Secure environment variable access
  • helpers - Access helpers from other extensions

Electron Extension APIs

Core Methods

  • registerIPCHandler(channel: string, handler: Function) - Register an IPC message handler
  • sendToRenderer(channel: string, data: any) - Send message to renderer process
  • registerMenuItem(menu: string, item: MenuItemDefinition) - Register a menu item
  • registerProtocol(handler: ProtocolHandler) - Register custom URL protocol handler
  • watchFileSystem(watcher: FSWatcherDefinition) - Watch file system changes
  • spawn(command: string, args: string[]) - Spawn a child process
  • exec(command: string) - Execute a shell command

Advanced APIs

  • storage - Extension-scoped persistent storage
  • app - Access to Electron app instance
  • mainWindow - Access to main BrowserWindow

Shared Types

All shared types and utilities are available from @voiden/sdk/shared.

For complete API documentation with detailed examples, see the TypeScript definitions or check out our examples.

Examples

The examples directory contains complete, working examples:

  • Hello World - Basic slash command and toast notifications
  • More examples coming soon!

Using Examples

Each example is a standalone project you can use as a template:

cd examples/hello-world
npm install
npm run build

Then copy the built extension to your Voiden extensions directory.


Voiden Runner (Headless)

The Voiden Runner allows extensions to execute headlessly in plain Node.js environments (CLI/CI). This is essential for automation and integration testing where no UI or Electron context exists.

For detailed information on how attributes are normalized, see the Block Schema Guide.

Runner Example (runner.ts)

import type { RunnerFactory, RunnerContext } from '@voiden/sdk/runner';

const myRunner: RunnerFactory = (context: RunnerContext) => {
  return {
    async onload() {
      // Define attribute defaults for headless normalization
      context.registerBlockSchema({
        name: 'my-custom-block',
        attrs: { url: { default: 'https://api.example.com' } }
      });

      // Build requests from document blocks
      context.onBuildRequest((request, blocks) => {
        const myBlock = blocks.find(b => b.type === 'my-custom-block');
        return myBlock ? { ...request, url: myBlock.attrs?.url, method: 'POST' } : request;
      });

      // Process responses (assertions/logging)
      context.onProcessResponse((response, blocks, request) => {
        if (response.status === 200) {
          context.report.add({ type: 'assertion', passed: true, message: 'Status 200 OK' });
        }
      });
    }
  };
};

export default myRunner;

Runner API Reference

Core Methods

  • registerBlockSchema(def: BlockSchemaDef) - Define block attributes and defaults for normalization
  • onBuildRequest(handler: RunnerRequestHandler) - Transform document blocks into a request state
  • onProcessResponse(handler: RunnerResponseHandler) - Evaluate results and emit report entries
  • pipeline.registerHook(stage, handler, priority?) - Register granular pipeline hooks (e.g., pre-send, post-processing)
  • report.add(entry: CliReportEntry) - Emit logs or assertions to the CLI/CI output

Context Properties

  • env - Flat map of resolved environment variables
  • verbose - Boolean flag indicating if verbose logging is enabled

Troubleshooting

Common Issues

TypeScript errors about missing types

  • Ensure all peer dependencies are installed: npm install react react-dom @tiptap/core @tiptap/pm @tiptap/react
  • Check that your tsconfig.json includes "moduleResolution": "node"

Extension not loading in Voiden

  • Verify your build output is in the correct format (ESM)
  • Check the browser console for error messages
  • Ensure your extension class is properly exported

IPC handlers not receiving messages

  • Verify the channel name matches between UI and Electron extensions
  • Check that the Electron extension is loaded before sending messages
  • Use await when sending IPC messages that expect responses

Build errors

  • Run npm run typecheck to see detailed TypeScript errors
  • Ensure you're using Node.js 20.x or higher
  • Clear dist folder and rebuild: rm -rf dist && npm run build

Getting Help

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development

# Clone the repository
git clone https://github.com/VoidenHQ/sdk.git
cd sdk

# Install dependencies
npm install

# Build the SDK
npm run build

# Watch mode for development
npm run dev

# Type checking
npm run typecheck

Security

Found a security vulnerability? Please see our Security Policy for reporting instructions.

License

MIT - see LICENSE file for details.

Community


Built with โค๏ธ by the Voiden team