Clean separation between core app infrastructure and extensions.
void/
├── apps/
│ ├── electron/ # Electron main process
│ │ └── src/
│ │ ├── main/ # IPC handlers, window management
│ │ └── preload.ts # Expose APIs to renderer
│ │
│ └── ui/ # React renderer app
│ └── src/
│ ├── core/ # CORE INFRASTRUCTURE
│ │ └── request-engine/ # HTTP execution
│ └── plugins/ # EXTENSIONS
│
├── core-extensions/ # AUTO-DISCOVERED EXTENSIONS
│ └── src/
│ ├── hello-world/
│ └── md-preview/
What belongs in core:
- Fundamental app features
- Performance-critical systems
- Security-sensitive operations
- Deep system integrations
Examples:
core/request-engine/- HTTP requests (PRIMARY PURPOSE of app)core/editor/- TipTap/ProseMirror foundationcore/environment/- Environment variable managementcore/panels/- Panel system
Why these are core:
- Request engine = Like an IDE's editor
- Editor = Foundation for all documents
- Environment = Security-sensitive credentials
- Panels = UI framework for everything
What belongs in extensions:
- Features built on top of core
- Protocol-specific UI (REST, GraphQL, etc.)
- Optional functionality
- User customizations
Examples:
voiden-rest-api/- REST-specific editor nodesmd-preview/- Markdown preview toggle
apps/electron/
├── main/
│ ├── files/ # File system operations
│ ├── git/ # Git integration
│ ├── terminal/ # Terminal (node-pty)
│ └── extension/ # Extension management
└── preload.ts # Expose APIs to renderer
Responsibilities:
- File system access
- Native dialogs
- Git operations
- Terminal emulation
- Extension loading
apps/ui/src/core/
├── request-engine/ # HTTP request execution
│ ├── hooks/ # React hooks
│ ├── utils/ # Pure functions
│ └── components/ # UI components
├── editor/ # (future) Editor core
├── environment/ # (future) Env management
└── panels/ # (future) Panel system
Responsibilities:
- Request execution engine
- Editor foundation
- State management
- UI framework
packages/core-extensions/src/
├── voiden-rest-api/ # REST API
├── md-preview/ # Markdown preview
└── (future extensions)
Responsibilities:
- Add editor nodes
- Add slash commands
- Add UI actions
- Add panels/sidebars
The SDK (@voiden/sdk) is an external npm package that provides the extension API.
Responsibilities:
- Define extension APIs
- Provide type safety
- Abstract app internals
User Action (Click Send / Cmd+Enter)
↓
SendRequest Component (core/request-engine/components/SendRequest.tsx)
↓
useSendRequest Hook (core/request-engine/hooks/useSendRequest.ts)
↓
requestOrchestrator.executeRequest (core/request-engine/requestOrchestrator.ts)
├── Step 1: Build request through plugin handlers
├── Step 2: sendRequestHybrid (core/request-engine/sendRequestHybrid.ts)
│ ├── UI Stage 1: Pre-processing hooks
│ ├── UI Stage 2: Request compilation hooks
│ ├── UI Stage 5: Pre-send hooks
│ ├── Electron IPC (window.electron.request.sendSecure)
│ │ ├── Stage 3: Env variable replacement
│ │ ├── Stage 4: Auth injection
│ │ ├── Stage 6: HTTP Request via Node.js
│ │ └── Stage 7: Response extraction
│ └── UI Stage 8: Post-processing hooks
└── Step 3: Process response through plugin handlers
↓
React Query Cache
↓
UI Updates (responseStore)
App Startup
↓
Electron: Load registry from @voiden/core-extensions
↓
Electron: Sync to user state (preserve enabled/disabled)
↓
UI: Import coreExtensionPlugins
↓
UI: Load each enabled extension
↓
Extension: onload() called with PluginContext
↓
Extension: Registers nodes/commands/actions
↓
Editor: Combines core + extension features
Question: Should sendRequest be in core or extension?
Answer: CORE ✅
Reasoning:
- HTTP requests are the PRIMARY PURPOSE of Voiden
- Deep integration with editor, environment, panels, electron
- Performance-critical and security-sensitive
- Foundation for other protocols (GraphQL, gRPC)
Implementation:
CORE (apps/ui/src/core/request-engine/)
└── HTTP execution engine
├── requestOrchestrator.ts # Plugin handler orchestration
├── sendRequestHybrid.ts # Hybrid pipeline execution
└── pipeline/ # Hook registry & stages
EXTENSION (apps/ui/src/plugins/voiden-api/)
└── REST-specific UI
├── Method node
├── URL node
└── Headers table
└── USES core request engine via SDK hooks
The request pipeline allows plugins to hook into different stages of request execution.
1. Request Orchestrator (High-level)
Used via SDK's PluginContext for building requests and processing responses:
// In your plugin's onload():
context.onBuildRequest(async (request, editor) => {
// Called BEFORE request is sent
// Modify or build the request object
request.headers.push({ key: 'X-Custom', value: 'value', enabled: true });
return request;
});
context.onProcessResponse(async (response) => {
// Called AFTER response is received
// Process, log, or react to the response
console.log('Response status:', response.status);
});
context.registerResponseSection({
name: 'my-section',
order: 10,
component: MyResponseComponent,
});2. Pipeline Hook Registry (Low-level)
For fine-grained control at specific pipeline stages:
import { hookRegistry, PipelineStage } from '@/core/request-engine/pipeline';
// Pre-processing: Validate or cancel request
hookRegistry.registerHook('my-extension', PipelineStage.PreProcessing, async (ctx) => {
if (!ctx.requestState.url) {
ctx.cancel(); // Abort the request
}
}, 50); // priority: lower runs first
// Request compilation: Add data to request
hookRegistry.registerHook('my-extension', PipelineStage.RequestCompilation, async (ctx) => {
ctx.addHeader('X-Timestamp', Date.now().toString());
ctx.addQueryParam('source', 'voiden');
});
// Pre-send: Last modifications before sending
hookRegistry.registerHook('my-extension', PipelineStage.PreSend, async (ctx) => {
ctx.metadata.startTime = performance.now();
});
// Post-processing: After response received
hookRegistry.registerHook('my-extension', PipelineStage.PostProcessing, async (ctx) => {
const duration = performance.now() - ctx.metadata.startTime;
console.log('Request took:', duration, 'ms');
});| Stage | Location | Extensible | Purpose |
|---|---|---|---|
PreProcessing |
UI | ✅ Yes | Validate, transform, cancel request |
RequestCompilation |
UI | ✅ Yes | Add headers, query params, build request |
EnvReplacement |
Electron | ❌ No | Replace {{variables}} (security) |
AuthInjection |
Electron | ❌ No | Add auth headers (security) |
PreSend |
UI | ✅ Yes | Final modifications, logging |
Sending |
Electron | ❌ No | Execute HTTP request |
ResponseExtraction |
Electron | ❌ No | Parse response |
PostProcessing |
UI | ✅ Yes | Cache, log, validate response |
// PreProcessing
interface PreProcessingContext {
editor: Editor;
requestState: RestApiRequestState;
cancel: () => void; // Call to abort request
}
// RequestCompilation
interface RequestCompilationContext {
editor: Editor;
requestState: RestApiRequestState;
addHeader: (key: string, value: string) => void;
addQueryParam: (key: string, value: string) => void;
}
// PreSend
interface PreSendContext {
requestState: RestApiRequestState;
metadata: Record<string, any>; // Share data between hooks
}
// PostProcessing
interface PostProcessingContext {
requestState: RestApiRequestState;
responseState: RestApiResponseState;
metadata: Record<string, any>;
}Question: Manual registration or auto-discovery?
Answer: AUTO-DISCOVERY ✅
Implementation:
- Each extension has
manifest.json - Build script scans folders
- Generates
registry.tsandplugins.ts - Electron syncs to user state on startup
Benefits:
- Zero configuration
- Single source of truth
- Always in sync
- User preferences preserved
Question: How to avoid "document is not defined" in Electron main?
Answer: Separate files
Implementation:
registry.ts- Metadata only (Node-safe)plugins.ts- Plugin map with imports (browser-only)
Electron imports registry, UI imports plugins.
- React - UI framework
- TypeScript - Type safety
- Vite - Build tool (handles TS compilation)
- TipTap - Rich text editor (ProseMirror wrapper)
- CodeMirror - Code editor
- Tanstack Query - Data fetching
- Tanstack Router - Routing
- Zustand - State management
- Radix UI - Unstyled components
- Tailwind - Styling
- Electron - Desktop framework
- simple-git - Git operations
- node-pty - Terminal emulation
- @voiden/sdk - Extension API types (external npm package)
- TypeScript - Compilation
apps/ui/src/core/request-engine/hooks/useSendRequest.ts
packages/core-extensions/src/md-preview/index.ts
// Core infrastructure
import { useSendRequest } from "@/core/request-engine";
// Extensions (auto-discovered)
import { coreExtensionPlugins } from "@voiden/core-extensions";
// SDK
import { PluginContext } from "@voiden/sdk/ui";// DON'T: Extensions importing app internals directly
import { something } from "@/internal/some-feature";
// DO: Use SDK
const theme = context.ui.getProseClasses();- Location:
packages/core-extensions/src/ - Discovery: Auto via
manifest.json - Dependencies: SDK only
- Examples:
hello-world,md-preview
- Location:
apps/ui/src/plugins/ - Registration: Manual in
coreExtensions.ts - Dependencies: Can import from app
- Example:
voiden-wrapper-api-extension
- Optimized and bundled with app
- Always loaded
- Performance-critical
- Lazy-loaded when needed (future)
- Can be disabled
- Lower priority
- Aggressive caching for fast dev
- Clear cache when packages change:
rm -rf apps/ui/node_modules/.vite
- Context isolation enabled
- Preload script exposes limited API
- No direct node access from renderer
- SDK provides controlled access
- No file system access (must use SDK)
- No process spawning
- Sandbox in future versions
- Handles auth tokens securely
- Environment variables isolated
- Credential storage encrypted
- Unit tests for utilities
- Integration tests for flows
- E2E tests for critical paths
- Unit tests (isolated)
- Easy to mock SDK
- Test independently
- Add more SDK APIs
- Improve extension hot-reload
- Extension marketplace
- Extension sandboxing
- Plugin versioning system
- Extension analytics
Voiden's architecture follows these principles:
- Core = Infrastructure - Fundamental features in
core/ - Extensions = Features - Optional functionality in
packages/core-extensions/ - Auto-Discovery - Extensions found via
manifest.json - Type Safety - TypeScript throughout
- Performance - Optimized core, lazy-loaded extensions
The architecture is designed to be:
- ✅ Maintainable - Clear separation of concerns
- ✅ Extensible - Easy to add features
- ✅ Performant - Core optimized, extensions optional
- ✅ Secure - Controlled API access
- ✅ Developer-Friendly - Clear patterns, good docs