--- name: obsidian-plugin-architecture description: >- Architecture for Obsidian plugins: pure core vs Vault adapter, vault-as-data, codeblocks, path safety, caching, and mobile/minAppVersion. Use when designing plugin structure, choosing where logic lives, or /obsidian-plugin-architecture. --- # Obsidian plugin architecture Obsidian loads one CJS file. Keep the parts that do not need the app testable in Node. Keep vault I/O behind a thin adapter. Bundle/esbuild belongs to [obsidian-plugin-scaffold](../obsidian-plugin-scaffold/SKILL.md). DOM/CSS/`data-testid` belong to [obsidian-plugin-ui](../obsidian-plugin-ui/SKILL.md). ## Layers ```text src/core.ts, src/core/* pure parse / model (no obsidian) src/util/* path safety, caches, layout helpers src/data/* Vault API adapter src/views/*, codeblocks markdown post-processors / live UI src/commands/*, settings palette + settings tab src/main.ts Plugin subclass: register, refresh, lifecycle ``` **Pure core.** Parsers, markdown writers, settings merge, and any math you can unit-test without a vault belong here. They import nothing from `obsidian`. Node's test runner loads them with `--experimental-strip-types`. If a function needs `App`, `TFile`, or `Notice`, it does not belong here. **Path and cache helpers.** Reject `..`, absolute paths, and drive letters. Scan prefixes end in `/` so `Gym` does not match `Gymnastics`. Drop list caches on vault events. Do not iterate `vault.getFiles()` to find a known path; use `getAbstractFileByPath` / `getFileByPath`. **Vault adapter.** Read with `cachedRead` / `getFileCache`. Write background files with `vault.process`. Edit frontmatter with `fileManager.processFrontMatter`. Prefer the Vault API over `adapter`. Views never open `adapter` for plugin data. **UI.** Register markdown post-processors or views. Debounce refresh on vault/metadata events via `this.registerEvent`. How to paint DOM is the UI skill. **Plugin class.** Load settings, register codeblocks/commands/settings, refresh on events. Conditional commands use `checkCallback` (or `editorCheckCallback`). Unconditional commands use `callback`. Vault-as-database patterns: [references/vault-as-data.md](references/vault-as-data.md). ## Vault as the database Default bias: notes and frontmatter are the store. `data.json` (plugin settings) holds paths, toggles, catalogs, and setup flags. If a value should survive a settings reset or be editable as text, it belongs in a note. ## Codeblocks (optional product surface) Some plugins expose features as fenced code blocks users paste into notes. If you do that: 1. Parser + defaults (comment lines starting with `#` ignored) 2. View / renderer 3. Fence comments in newly created notes and examples 4. User-facing docs 5. Selenium if the option is user-visible Keep legacy language aliases only while a migrate path still needs them. Plugins that use custom views or the editor instead can skip this section. ## Mobile and minAppVersion `isDesktopOnly: false` unless you call Node or Electron APIs. Avoid lookbehind in regexes if you claim mobile support. Call APIs newer than `minAppVersion` only behind a feature detect, with a fallback that still works on `minAppVersion`. Example: implement both `display()` (pre-1.13 settings) and `getSettingDefinitions()` (1.13 settings search). Do not call APIs newer than your floor without raising `minAppVersion` or feature-detecting. ## Privacy Default to local-only. No telemetry, accounts, or ads. Document any network fetch in the README (why it exists, what leaves the vault). ## Anti-patterns This is the canonical Avoid/Prefer table. UI checklist tests encode a subset as source assertions; do not copy this table into other skills. | Avoid | Prefer | | --- | --- | | Domain logic importing `obsidian` | Pure `src/core/*` | | `vault.getFiles().find(path === …)` | `getFileByPath` / `getAbstractFileByPath` | | `vault.modify` on a background file | `vault.process` | | Hand-rolled YAML frontmatter | `fileManager.processFrontMatter` | | Global `app` / `window.app` in plugin source | `this.app` | | `workspace.activeLeaf` | `getActiveViewOfType`, `activeEditor` | | Detach leaves in `onunload` | Leave custom views where the user put them | | Holding a long-lived `this.view = new MyView()` | Factory that returns a new view; query leaves when needed |