The Two-Tier Model
Asyar extensions come in two tiers. Understanding this distinction shapes every architectural decision you make.
┌─────────────────────────────────────────────────────────────────────┐
│ ASYAR HOST PROCESS (Tauri) │
│ │
│ ┌─────────────────────────────┐ ┌───────────────────────────┐ │
│ │ TIER 1: BUILT-IN │ │ TIER 2: INSTALLED │ │
│ │ FEATURES │ │ EXTENSIONS │ │
│ │ │ │ │ │
│ │ Calculator, Snippets, │ │ Your extension │ │
│ │ Shortcuts, AI Chat, │ │ Any third-party ext │ │
│ │ Portals, Create Extension │ │ │ │
│ │ │ │ ┌─────────────────────┐ │ │
│ │ Runs IN host context. │ │ │ <iframe> sandbox │ │ │
│ │ Direct access to all │ │ │ asyar-extension:// │ │ │
│ │ Tauri APIs. No sandbox. │ │ │ │ │ │
│ │ isBuiltIn: true │ │ │ postMessage bridge │ │ │
│ │ │ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ └───────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ RUST CORE (Tauri 2) │ │
│ │ Discovery · Lifecycle · Permissions · URI Scheme · Search │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Tier 1 — Built-in Features
- Location: Reside directly within the source tree at
src/built-in-features/*/. - Loading: Discovered using Vite's
import.meta.globduring the build phase. The JavaScript modules are fully bundled. - Context: They run directly within the Privileged Host Context (the same
windowobject as SvelteKit). - Execution: Flagged internally as
isBuiltIn: true. They export a standard Svelte component via falling back through module keys (typicallyDefaultView). They can directly access Tauri commands and internal DOM elements without serialization overhead. - Convention: The component must be exported as
DefaultViewto correctly match routing identifiers.
Tier 2 — Installed Extensions
- Location: Reside dynamically on the OS-specific application data directory (macOS:
~/Library/Application Support/org.asyar.app/extensions/, Windows:%APPDATA%/org.asyar.app/extensions/, Linux:~/.local/share/org.asyar.app/extensions/). - Loading Strategy: Manifest-only. When the Host application starts,
ExtensionLoaderServiceparses theirmanifest.jsonfiles and extracts the commands, but explicitly setsmodule: null. The host application never evaluates or imports a Tier 2 extension's JavaScript directly in the main window. - Context: Flagged as
isBuiltIn: false. They execute entirely within an isolated<iframe>sandbox. - Deferred Execution: The code environment for a Tier 2 extension only boots when a user executes a specific command mapped to that extension, causing the Host to construct and mount the sandbox
<ExtensionIframe>. - Bootstrap pattern: A Tier 2 extension's
main.tscreates its ownExtensionContextand callssetExtensionId(id). That single call wires the context into the iframe'sExtensionBridgesingleton (viaregisterActiveContext), patches all service proxies with the extensionId, and drains any already-delivered preferences bundle. The extension can then callcontext.getService<T>(name)or readcontext.preferences.*immediately. See Preferences → How the bundle reaches the live context and the IPC bridge preferences section. - Theme extensions (
type: "theme"): A sub-class of Tier 2 that contains no JavaScript. Theme packages declare CSS variable overrides and optional custom fonts intheme.json.ExtensionLoaderServicediscovers theme extensions normally but skips them during module loading — they never get an iframe. Theme application is handled exclusively bythemeService.ts, which reads the theme definition via theget_theme_definitionRust command and applies CSS variables directly todocument.documentElement.
Historical note — why not dynamic import()
- Why not
dynamic import()in the host window? An earlier implementation loaded Tier 2 extensions viaimport(/* @vite-ignore */ 'asyar-extension://{id}/dist/index.js')directly into the host window. This caused three cascading failures: (1) the extension bundled its own copy ofMessageBroker, creating a duplicate singleton that never correctly bound to the host's state, (2)window.parent === windowin the same execution context, causingpostMessagecalls to loop back to the sender, and (3)extensionIdcontext from the host's injectedExtensionContextwas ignored because the extension's internally bundled SDK instance was a different object in memory. The iframe model eliminates all three problems by giving each extension a genuinely separate JavaScript execution context.
See also: IPC bridge · Permission system