Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plugin System Design

This document defines the intended plugin-system design layered on top of the current Plugin Runtime and Community Plugin Host Boundary contracts. It is written as a spec for the target model, while noting where the current implementation already conforms and where later phases still remain.

Scope

The plugin system preserves the Obsidian-compatible community-plugin disk layout while adding Lapis-specific manifest metadata, contribution indexing, host selection, and capability-aware execution. PluginManager remains the lifecycle authority for discovery, preflight, activation, deactivation, rollback, diagnostics, and boot ordering.

The design covers:

  • manifest shape and classification
  • contribution indexing and activation
  • service-provider registration
  • runtime-host selection and runtime entries
  • permissions and trust language
  • install layout and extension state
  • system extensions and isolation boundaries

Design Principles

  • Preserve baseline Obsidian community-plugin compatibility.
  • Keep one lifecycle owner for discovery, preflight, diagnostics, and cleanup.
  • Prefer declarative contributions over eager code execution.
  • Treat host permissions as enforceable only where access is brokered or otherwise restricted.
  • Keep host and contribution models stable as desktop isolation improves.

The UI and host split is intentional:

  • existing Obsidian-compatible community plugins continue to run through the trusted DOM compatibility host so the on-disk plugin format and runtime model remain usable
  • new Lapis extensions should prefer manifest declarations, lazy activation, commands, context keys, brokered capabilities, and isolated UI or runtime hosts where possible instead of arbitrary renderer DOM mutation

This means compatibility APIs remain important, but they are not the design target for new extension surfaces.

Manifest Model

The baseline Obsidian manifest remains the compatibility contract. Lapis-specific metadata lives under an optional lapis namespace so upstream fields do not need to move or change meaning.

{
  "id": "example",
  "name": "Example",
  "author": "Author",
  "version": "1.0.0",
  "minAppVersion": "1.12.3",
  "description": "A Lapis extension",
  "lapis": {
    "manifestVersion": 1,
    "extensionKind": ["workspace", "trustedDesktop"],
    "contributes": {
      "commands": [],
      "configuration": [],
      "languages": [],
      "editorViews": [],
      "views": [],
      "services": []
    },
    "activationEvents": [],
    "runtime": {
      "workspace": "main.js",
      "desktop": "desktop.js"
    },
    "permissions": []
  }
}

The lapis namespace defines contribution declarations, activation events, runtime entries, and permission requests. Missing lapis metadata means the plugin is treated as an Obsidian-compatible community plugin.

Classification

Each discovered plugin is classified before runtime preflight decides which files are required.

ClassDefinitionRequired entryIntended behavior
Obsidian-compatibleBaseline manifest with no lapis namespacemain.jsPreserve current behavior
Lapis extensionManifest uses only Lapis contribution and runtime metadataSelected Lapis runtime entry, if anyAllow declarative and host-specific extensions
HybridBaseline Obsidian plugin plus lapis metadatamain.js plus any selected Lapis runtime entryAllow gradual adoption of the Lapis model

Classification rules are:

  1. If lapis is absent, classify as Obsidian-compatible.
  2. If lapis is present and main.js is still required for compatibility behavior, classify as hybrid.
  3. If lapis is present and the plugin can be expressed as declarative metadata plus optional Lapis runtime entries, classify as a Lapis extension.
  4. If classification is ambiguous, preflight fails instead of guessing.

Current implementation status: manifest classification is implemented.

Contribution Model

PluginManager builds a contribution index from manifests before evaluating plugin code. The index is the canonical source for diagnostics, settings visibility, manifest-only installation, and later lazy activation.

Indexed contributions may include:

  • commands
  • configuration schemas
  • status bar items
  • menus and keybindings
  • language IDs and file extensions
  • language-service providers
  • workspace and sidebar views
  • tree views and view-scoped badges
  • webviews and prompt surfaces
  • markdown processors and code-block processors
  • notebook renderers and kernels
  • service-provider declarations

Each indexed contribution record carries plugin ID, contribution type, stable contribution ID, manifest source path, runtime host requirement, permission requirements, activation requirement when code is needed, and validation diagnostics.

Built-in contribution kinds are now defined through contribution-point descriptors instead of being expanded inline in one hardcoded index builder. A descriptor is responsible for validating the contribution shape, producing indexed entries, declaring whether the contribution is manifest-installable, and supplying default activation metadata when the contribution kind supports lazy activation. Unknown contribution keys and malformed contribution records remain in diagnostics as invalid indexed entries so plugin settings can explain what failed without silently dropping manifest data.

Declarative contributions now also share an API-owned context-key service. App.contextKeys is the single source of truth for built-in state such as view.id, view.focused, editor.active, editor.language, editor.hasSelection, workspace.trusted, runtime.host, runtime.desktop, runtime.browser, runtime.nativeHost, plugin.state.<pluginId>, and plugin.enabled.<pluginId>. Plugins can register scoped custom keys under plugin.<pluginId>.*, and plugin unload resets those scoped keys automatically.

Supported when grammar for declarative contribution gating currently includes:

  • identifiers using dot or dash namespaces, such as editor.active or plugin.enabled.my-plugin
  • &&, ||, and unary !
  • == and !=
  • parentheses for grouping
  • boolean literals plus quoted string and number literals

The first declarative consumer is command visibility: manifest command contributions can declare when, and the command palette hides or shows those commands through the shared evaluator before callback-specific availability checks run.

Manifest contributions now also support statusBarItems, with records shaped as { id, text?, icon?, alignment?, priority?, tooltip?, command?, when? }. These items install without evaluating plugin code, render through the shared workspace shell on the declared alignment track, and execute referenced commands through the normal command registry so deferred command activation still works. The existing Plugin.addStatusBarItem() raw DOM API remains available as the compatibility escape hatch for Obsidian-style plugins.

Manifest contributions also support editorViews, with records shaped as { id, viewType?, label, description?, filenamePatterns, priority?, activationEvent? }. They install metadata into the workspace editor-view registry without evaluating plugin code, so settings can present selectable editor IDs and configured workspace.editorAssociations values can be preserved even when the backing runtime view is disabled. A code-backed plugin still registers the actual view constructor through Plugin.registerView(); manifest metadata only describes selectable file-backed editor ownership.

Current implementation status: contribution indexing is implemented. Manifest-only installation covers commands, configuration, status bar items, editor-view metadata, and bundled system-extension service bindings, starting with language services.

Follow-on contribution surfaces are intentionally staged behind the same model rather than being introduced as separate ad hoc registries. The next planned surfaces tracked in the backlog are:

  • codicon- and registry-icon label text rendering for declarative contribution labels (command palette today; status bar, trees, and quick pick next)
  • generalized contribution-point descriptors and validation
  • context keys and when clauses
  • declarative status bar items, menus, and keybindings
  • tree views, view badges, and view-scoped context keys
  • output channels, quick pick or input prompts, and richer declarative configuration controls
  • webviews and expanded host-kind support such as browserWorker

When adding a new contribution point, extend the built-in descriptor registry with:

  • shape validation for each contributed record
  • stable contribution IDs and required permission metadata
  • default activation-event metadata when the contribution can wake code lazily
  • manifest-installation metadata so renderer-owned surfaces can install without eager plugin code
  • diagnostics for malformed records and unsupported manifest use

Activation Model

Code execution is delayed until a contribution needs behavior that cannot be installed from manifest data alone. PluginManager remains the coordinator for all activation decisions.

Initial activation events are:

EventMeaning
onStartupFinishedActivate after workspace boot finishes
onCommand:<id>Activate before running a contributed command
onLanguage:<languageId>Activate when a matching document is opened
onView:<viewType>Activate before creating a contributed view
onFileSystem:<glob>Activate when matching vault files are present or opened
onService:<serviceId>Activate when a service provider is selected
workspaceContains:<glob>Activate after discovery finds a matching file

Default behavior is host- and class-dependent:

  • Obsidian-compatible plugins activate during the normal plugin boot phase.
  • Lapis extensions default to lazy activation unless they declare startup work or own contributions needed during boot.
  • Hybrid plugins preserve main.js compatibility and may still require eager activation.

The boot invariant remains unchanged: layout restoration must not race ahead of required plugin activation when code-backed views or services are needed for layout recovery.

Current implementation status: activation-event lazy activation is implemented for indexed Lapis extensions, including command-triggered activation, layout-safe view activation before restore, language/service activation hooks, and vault-path activation for workspaceContains and onFileSystem patterns.

Future declarative UI contributions must keep the same activation rule: the app should be able to index and, when practical, install metadata without forcing plugin code evaluation, then activate code only when the contribution actually needs behavior.

Service Model

Service extensions register as scoped providers owned by the target service rather than replacing App services. Runtime registration returns a disposer and is tied to plugin lifecycle cleanup.

A service-provider declaration includes:

  • provider ID
  • owning plugin ID
  • service ID
  • runtime host
  • required capabilities
  • priority
  • activation event
  • permission requirements

Service IDs remain namespaced. Duplicate provider IDs for the same service are rejected unless the owning service explicitly allows replacement.

Current implementation status: language-service providers follow this model today.

Runtime Hosts

Community plugin code runs through a runtime-neutral execution-host seam.

  • The renderer host is the compatibility fallback for browser/PWA and for community plugins that still run in process.
  • The Electron sidecar host is the trusted desktop host for brokered desktop execution.
  • PluginManager stays responsible for discovery, preflight, diagnostics, activation state, and renderer-installed contributions regardless of which host executes plugin code.

The MVP desktop topology is:

Workspace renderer
  -> PluginManager
    -> preload desktop_plugin_host_* IPC
      -> Electron main PluginSidecarManager
        -> forked Node child process
          -> extension code and broker requests

Electron main owns process creation, timeout handling, crash recovery, shutdown, and brokered native capability access. The renderer owns policy and UI-visible state.

Current implementation status: renderer execution and trusted desktop sidecar execution are both implemented.

Runtime Entry Selection

Lapis runtime entries are separate from main.js. The selected entry depends on plugin class, active runtime, and the narrowest host that can satisfy the contribution.

{
  "lapis": {
    "runtime": {
      "workspace": "main.js",
      "browserWorker": "worker.js",
      "desktop": "desktop.js"
    }
  }
}

Runtime-entry rules are:

  1. Obsidian-compatible plugins use main.js.
  2. Hybrid plugins keep main.js and may additionally declare Lapis runtime entries.
  3. Renderer-hosted Lapis code uses lapis.runtime.workspace.
  4. Sidecar-hosted desktop code uses lapis.runtime.trustedDesktop or lapis.runtime.desktop.
  5. Only the selected runtime entry is required for a given host decision.
  6. Selected host mode and runtime entry are recorded in diagnostics.

All manifest-declared runtime entries must be relative to the plugin folder, must not escape the plugin root, and currently must use .js, .cjs, or .mjs extensions.

Current implementation status: workspace and trusted desktop runtime-entry selection is implemented. browserWorker and multi-host hybrid activation remain future work.

Permissions And Trust

Permissions are manifest-declared requests and are enforceable only for APIs that go through a broker or another restricted host surface.

PermissionBrokered capability
vault.readvault:read
vault.writevault:write
plugin.dataplugin:data
commandscommands
noticesnotices
settings.readsettings
metadata.querymetadata:query
eventsevents
logginglogging

Preflight evaluates declared permissions against the selected host. Missing or unavailable required permissions fail before code evaluation. Granted permissions and host mode are surfaced in diagnostics.

Trusted desktop code is not sandboxed by these app permissions. Once arbitrary Node code is allowed to run in a trusted sidecar, the permission UI must describe those grants as app API access, not as an OS-level sandbox.

Current implementation status: requested and granted permissions plus trusted-host warnings are surfaced in the settings UI.

Install Layout

The plugin system preserves the vault-local community-plugin install layout.

.obsidian/
  community-plugins.json
  plugins/
    <plugin-id>/
      manifest.json
      main.js
      desktop.js
      worker.js
      styles.css
      data.json
      assets/

Only the files required by the plugin class and selected runtime need to exist. Manifest-only Lapis extensions may contain only manifest.json and static assets. Mutable plugin state stays separate from shipped code and assets.

Extension State

The design separates installed package files, user-authored configuration, and generated runtime state.

StateCurrent locationPurpose
Package files.obsidian/plugins/<id>/Manifest, entries, CSS, static assets
Community plugin dataapp.json pluginData (canonical), mirrored to .obsidian/plugins/<id>/data.jsonCanonical plugin compatibility persistence plus Obsidian layout compatibility
Bundled plugin settingsapp.json pluginData (canonical), mirrored to .obsidian/<id>.jsonCanonical bundled/system plugin compatibility persistence plus legacy file compatibility
Generated app stateApp database outside canonical notes when availableSearch, metadata, notebook outputs, derived records

Compatibility loadData() and saveData() behavior remains valid for community plugins. Rebuildable indexes, caches, and large derived artifacts should move to app-owned generated-state APIs rather than being written into shipped plugin folders.

Current implementation status: compatibility plugin data is canonicalized in app.json pluginData and mirrored back into the legacy Obsidian plugin files. Plugins also have generated-state helpers backed by AppDatabase metadata plus an idempotent migration helper for moving rebuildable subtrees out of compatibility settings payloads.

System Extensions

System extensions are app-shipped, app-versioned, trusted components that are not installed into a user’s vault as community plugins. They may use stronger privileges than community plugins, but those privileges remain explicit rather than implicit.

System extensions reuse the same contribution and service-registration model where possible so the application does not evolve two unrelated extension systems. Their settings surface remains distinct from the community plugin list.

Examples include native language-service providers, notebook kernels, file-watching providers, app-owned search backends, and trusted desktop integrations.

Current implementation status: bundled system extensions can now be registered at app boot as manifest-only extensions, renderer code-backed system plugins, or declarative services with app-owned implementations, all through explicit registration. Packaged discovery remains future work.

Security And Isolation

The desktop trusted host is powerful. A normal Electron child-process sidecar can access Node APIs unless the runtime deliberately restricts them. App-level permission checks do not make arbitrary Node code safe.

The design therefore distinguishes:

  • declarative and manifest-only extensions, which can be validated before code execution
  • renderer-hosted compatibility plugins, which preserve behavior but do not gain process isolation
  • trusted desktop extensions, which receive brokered app APIs but still require honest trust language

Isolation hardening may add Node permission flags, per-plugin sidecars, trust-level grouping, watchdogs, and packaged-app verification across supported platforms. Those later phases must not change the manifest, contribution, or host-selection model.

MVP Phases

  1. Manifest classification and contribution indexing.
  2. Manifest-only commands and configuration.
  3. Scoped service registration, starting with language services.
  4. Trusted desktop Electron sidecar lifecycle and brokered capability facades.
  5. Permission and trust diagnostics in the settings UI.
  6. Bundled system extensions with manifest-only contributions and service bindings.
  7. Isolation hardening and the remaining runtime-model work.

Current implementation status: phases 1 through 7 are complete for the current scoped host model. Follow-on runtime expansion such as browserWorker support, richer manifest-only coverage, and multi-host hybrid activation remains future work without changing the completed MVP boundary.