Plugin API and lifecycle
This page describes the PhialsPlugin container, lifecycle, PluginAPI, and how providers attach to the host — from an author perspective. Complete type definitions live in the phials-plugin-example repo under sdk/ (start with sdk/phials-plugin-sdk.d.ts).
Prerequisites: Plugin system overview · Getting started.
Plugin container
A PhialsPlugin bundles metadata, providers, and optional settings schema and database schema. Lifecycle hooks receive the same PluginAPI instance the host created when your plugin was registered.
interface PhialsPlugin {
id: string;
name: string;
version: string;
icons?: string[];
settings?: PluginSettingsSchema;
database?: PluginDatabaseSchema;
onActivate?: (api: PluginAPI) => void | Promise<void>;
onDeactivate?: () => void | Promise<void>;
onBeforeReload?: () => unknown | Promise<unknown>;
onAfterReload?: (state: unknown) => void | Promise<void>;
providers: PluginProvider[];
}See PhialsPlugin and PluginProvider in sdk/plugin-types.generated.d.ts.
Minimal example (community-style)
Your src/main.ts default export should be a function that returns a PhialsPlugin (the loader may call that factory). Pattern:
export default function createPlugin(): PhialsPlugin {
return {
id: "acme.my-plugin",
name: "My plugin",
version: "1.0.0",
providers: [],
async onActivate(api) {
const last = await api.storage.get<number>("runs");
await api.storage.set("runs", (last ?? 0) + 1);
},
};
}Use a real reverse-DNS id (see Community plugins); the snippet is illustrative.
Lifecycle (host behavior)
When the host registers your plugin, it constructs a PluginAPI for your id (and optional database schema). Activation then:
- Loads persisted settings (defaults from your schema + stored values).
- Creates database tables when a schema is present.
- Preloads icons if declared.
- Registers each provider with the registry that matches its
type. - Calls
onActivate(api)and marks the plugin active.
Deactivation runs onDeactivate, removes all providers, clears shortcuts and event subscriptions registered under your plugin id, clears the in-memory settings cache (persisted settings remain), and marks the plugin inactive.
Reload (when supported) calls onBeforeReload, deactivates, activates again, then onAfterReload(state).
sequenceDiagram
participant Host as Phials host
participant Reg as Registries
Host->>Host: activate plugin id
Host->>Host: settings / DB / icons
Host->>Reg: register providers by type
Host->>Host: onActivate apiFor community plugins, Phials exposes a permission-gated PluginAPI — only the surfaces and invoke commands allowed by your manifest are available. See Community plugins.
Provider routing
Each PluginProvider carries a type discriminant. The host forwards it to the matching subsystem (preview, metadata, view, module, theme, command, and legacy context / toolbar / selection). Prefer command for new toolbar, context menu, selection bar, and command-bar actions — see Command providers.
Command-related types (Command, CommandProvider, CommandContext, placements) are in sdk/command-types.generated.d.ts.
PluginAPI surface
The exact member list and typings are in sdk/plugin-types.generated.d.ts. Conceptually:
| Area | Role |
|---|---|
settings | Typed get / async set / getAll backed by host state |
storage | Async key-value store scoped to your plugin id |
database | SQL helpers when a database schema exists; otherwise the host may expose no-op stubs that throw |
appSettings | Read-only view of select global app settings |
invoke | Tauri command bridge; allow-listed; for community plugins the list is only what your manifest permissions allow (plus a small baseline) |
modal, notify | Dialogs and feedback hooks |
files | Path helpers |
events | Subscribe and emit on the shared event bus |
Extended surfaces (PreviewAPI, MetadataAPI, etc.) are merged in when the host calls provider hooks; see the PluginAPI union and provider props types in the SDK.
Scoped invoke (community)
Only invoke targets implied by your manifest permissions (plus a small always-allowed baseline) are permitted. Anything else errors at runtime. See Community plugins for the permission map.
Example: allowed read-style invoke
async function listDir(api: PluginAPI, path: string) {
const rows = await api.invoke<Array<{ path: string }>>("read_directory", { path });
return rows;
}Whether this succeeds for a community plugin depends on filesystem.read (and the host version). Validate against your manifest and test in Phials.
Settings
Settings are persisted under a per-plugin key in host state. activate merges schema defaults with stored values. deactivate clears the cache only — values remain on disk unless the user or uninstall flow deletes them.
await api.settings.set("favoriteExt", ".md");
const ext = api.settings.get<string>("favoriteExt");
const all = api.settings.getAll();Storage
api.storage is a separate async key-value store from settings (different backing and semantics). Keys are namespaced by the host so plugins cannot collide.
Database
When database on PhialsPlugin defines tables, the host provisions prefixed table names in the shared plugins database file. The api.database helpers accept logical short table names in SQL; the host rewrites them to prefixed physical names for common statement shapes.
Without a schema, api.database may throw if invoked — see typings in the SDK.
Events
api.events lets you subscribe to core and plugin events and emit your own. Handlers are tracked per plugin so deactivation can unsubscribe cleanly. To declare custom event names in TypeScript, augment PluginEvents as documented next to PluginEvents in sdk/events-types.generated.d.ts.
api.events.on("core.navigation.changed", ({ path, paneId }) => {
void path;
void paneId;
});Pitfalls
notifymay not map to visible toasts in all builds — treat as best-effort feedback unless documented otherwise for your target version.invokebehavior depends entirely on your declared permissions — test against a real manifest in Phials.- Unusual raw SQL shapes might not rewrite table names — prefer straightforward
FROM/JOIN/UPDATEpatterns.