Stream Kit Docs
Developers

Plugin getting started

This guide walks you through creating your first Stream Kit plugin from the template through local development and distribution.

Plugin getting started

This guide walks you through creating your first Stream Kit plugin from the template through local development and distribution.

For the full API reference, see Plugin authoring API. For installing plugins as a user, see Installing plugins.

What you build

A Stream Kit plugin is:

  1. A manifest.json with a stable key
  2. A single ESM entry file (dist/index.js) that default-exports a Plugin function
  3. Optional handlers, triggers, menu pages, settings, and lifecycle hooks

At runtime the app loads your bundle and calls your plugin with a PluginAppApi instance (app).

Step 1 — Use the template

Create a new repository from the plugin-starter GitHub template, or clone it manually:

gh repo create my-stream-kit-plugin --template stream-kit-app/plugin-starter --public

Install dependencies:

pnpm install

Start from src/minimal.ts for a short example, or use src/index.ts when you need the full declarative page block showcase.

Add the SDK packages as dev dependencies (included in the template):

pnpm add -D @stream-kit/plugin
pnpm add @stream-kit/core

For custom Svelte views or dashboard widgets (built-in-style plugins only), also add:

pnpm add -D @stream-kit/ui

Zip plugins that use declarative page blocks only do not need @stream-kit/ui.

Monorepo contributors: The same template source lives in packages/plugin-template/ for internal development inside the Stream Kit repository.

Update manifest.json:

{
	"key": "hello-world",
	"name": "Hello World",
	"version": "0.1.0",
	"entry": "dist/index.js"
}

Rules for key:

  • Lowercase letters, numbers, and hyphens only
  • Used for install paths, settings files, and handler/trigger IDs
  • Do not change it after release without a migration plan

Step 2 — Write the plugin entry

import type { Plugin } from '@stream-kit/plugin';

import { createGreetHandler } from './handler/greet';

const plugin: Plugin = (app) => ({
	name: 'Hello World',
	description: 'My first plugin',
	icon: 'ri:hand-heart-line',
	handlers: [createGreetHandler()],
	onEnable: () => {
		app.toast.create({
			title: 'Hello World',
			description: 'Plugin enabled',
			variant: 'success'
		});
	}
});

export default plugin;

The Plugin function receives app once at registration time. Return handlers, triggers, menuItems, settings, and lifecycle hooks from that function.

Persist settings with PluginStore

Most plugins store configuration in the lifecycle store (plugin.{key}.json):

onLoad: async ({ store }) => {
	const saved = await store.get<MySettings>('settings');
	if (saved) applySettings(saved);
},
onSave: async ({ store }) => {
	await store.set('settings', getSnapshot());
}

You do not need SQLite unless you have a specific reason to use app.db.

Step 3 — Build and externalize host modules

Plugins bundle to one ESM file. Do not bundle modules the app provides at runtime:

external: [
	'@stream-kit/plugin',
	'@stream-kit/plugin/action',
	'@stream-kit/core',
	'svelte',
	'@stream-kit/ui',
	'bits-ui',
	'runed',
	'@iconify/svelte'
]

In the monorepo, use the shared Vite config from plugins/create-vite-build-config.js. Standalone projects use the bundled vite.build.config.js from the plugin-starter template.

Build output:

dist/
└── index.js

Step 4 — Run locally in Stream Kit

  1. Start Stream Kit (desktop app)
  2. Build your plugin in watch mode: pnpm dev
  3. Link the plugin:
    • Open Plugins, enable Developer mode in Settings
    • Click Link dev plugin and select your manifest.json
  4. Enable the plugin on the Plugins page
  5. Enable Dev mode on the plugin card

When dist/index.js rebuilds, the app mirrors the output and reloads the plugin.

First-party plugins listed in dev-plugins.json are linked automatically during monorepo development.

Step 5 — Package and distribute

Create a zip:

pnpm package

Zip layout:

plugin.zip
├── manifest.json
└── dist/
    └── index.js

Users install the zip from the Plugins page. See Installing plugins for the user flow; publish updateManifestUrl in your manifest for self-hosted updates.

Import conventions

NeedImport fromExample
Types (Plugin, PluginAppApi, handlers, triggers)@stream-kit/pluginimport type { Plugin } from '@stream-kit/plugin'
Runtime helpers@stream-kit/coreimport { getFieldValue } from '@stream-kit/core'
BaseDirectory, SeekMode@stream-kit/pluginimport { BaseDirectory } from '@stream-kit/plugin'
Svelte UI (custom views/widgets only)@stream-kit/uiimport { Button } from '@stream-kit/ui/button'
Platform APIsapp parameterapp.fs.readTextFile(...), app.toast.create(...)

Never import @tauri-apps/* in plugin code. The app owns all platform logic.

@stream-kit/plugin types come from the published dist/index.d.ts. At runtime the app resolves @stream-kit/plugin to /plugin-host/plugin.js through the import map.

IDE hover and autocomplete

API documentation is embedded as JSDoc in @stream-kit/plugin and @stream-kit/core. Use import type for types and hover on app. methods, getFieldValue, BaseDirectory.AppData, and handler field definitions to see descriptions and examples in your editor.

Filesystem (app.fs)

Use app.fs for reading and writing files. Paths are usually relative to a BaseDirectory:

import { BaseDirectory } from '@stream-kit/plugin';

await app.fs.mkdir('logs', { baseDir: BaseDirectory.AppData, recursive: true });

await app.fs.writeTextFile('logs/events.log', `${line}\n`, {
	baseDir: BaseDirectory.AppData,
	append: true
});

const exists = await app.fs.exists('logs/events.log', {
	baseDir: BaseDirectory.AppData
});

Common directories:

BaseDirectoryTypical use
AppDataPlugin-owned data under the app data folder
AppConfigConfiguration files
AppCacheCache or temporary derived files
ResourceBundled app resources

For native file pickers, use app.fs.select({ type: 'file' | 'folder', filters?: [...] }). For a save dialog, use app.fs.save({ defaultPath?, filters? }).

To work with a file handle:

const file = await app.fs.open('data.bin', {
	read: true,
	baseDir: BaseDirectory.AppData
});

try {
	const buffer = new Uint8Array(1024);
	await file.read(buffer);
} finally {
	await file.close();
}

Zip plugins vs built-in plugins

FeatureZip / external pluginBuilt-in monorepo plugin
Declarative menu pages (blocks)YesYes
Handlers and triggersYesYes
PluginStoreYesYes
Custom Svelte views (customViews)NoYes
Svelte in menu page contentNoYes (built-in only)

Zip plugins should use declarative page blocks (text, form, button, card, …). Button blocks may define onClick for plugin-owned behavior.

Next steps

On this page