Build on Sunflare
Writing Plugins
Plugins are local, desktop-only scripts that add a small piece of UI to the app (a composer button, a right-click context menu item) without ever touching the network, your messages, or your account. No server, no publishing, no review process: install a .js file and it runs.
A plugin runs fully sandboxed in its own iframe with no network access at all, no access to the app's DOM, and no Node/Electron access. It can never read your messages, act on your behalf, or reach the network. There's no code path that lets it do any of that.
Install a plugin
Settings → Plugins (desktop app only) has an Install Plugin button that opens a native file picker filtered to .js files. Each installed plugin gets a toggle to enable/disable it and a remove action. Open Plugins Folder opens the actual folder on disk if you'd rather manage files by hand.
Anatomy of a plugin
A plugin is one JavaScript file starting with a manifest comment block:
// ==SunflarePlugin==
// @id your.unique.id
// @name My Plugin
// @version 1.0.0
// @author Your Name
// @description What it does, in one line
// ==/SunflarePlugin==
// your code goes here
Only @id is required. Install fails without it. Everything else defaults (name → "Untitled Plugin", version → "0.0.0", author → "Unknown"). All fields are length-capped, so keep them short.
The sunflare API
Inside a plugin, a global sunflare object is the entire surface you have to work with:
| Call | Does |
|---|---|
sunflare.addComposerButton({ id, icon, title }) | Adds a button to the message composer's icon row. Max 3 per plugin. |
sunflare.addContextMenuItem({ id, label }) | Adds an item to the message right-click menu. Max 3 per plugin. |
sunflare.onClick(id, callback) | Fires when the composer button or context-menu item with that id is clicked. The callback gets no arguments: no message content, no user data, just "it was clicked." |
sunflare.notify({ title, body }) | Shows a real desktop notification. Title capped at 100 characters, body at 300. Limited to 10 per minute. Going over disables the plugin. |
sunflare.playSound(dataUri) | Plays a short sound. Must be a data:audio/mpeg, data:audio/wav, or data:audio/ogg base64 URI, ~300KB or smaller. |
A complete example
This is public/plugins/example-plugin.js from the repo, verbatim. Install it as-is to see the whole thing work:
// ==SunflarePlugin==
// @id sunflare.example.hello
// @name Hello Plugin
// @version 1.0.0
// @author Sunflare
// @description Adds one composer button that fires a desktop notification.
// ==/SunflarePlugin==
sunflare.addComposerButton({ id: 'hello-btn', icon: 'star', title: 'Say hi' });
sunflare.onClick('hello-btn', () => {
sunflare.notify({ title: 'Hello Plugin', body: 'Composer button clicked.' });
});
What a plugin can't do
This is enforced structurally, not just by convention. There's simply no bridge for any of it:
- Read message content, DMs, or anything else in the app's DOM
- Send a message, act on your behalf, or call any Sunflare API route
- Make a network request of any kind (the sandbox's Content-Security-Policy blocks it outright)
- Access your account, trust rank, subscription, or any locally stored data
- Touch the filesystem or any Node/Electron API
Composer-button and context-menu clicks intentionally carry no payload. A plugin finds out a button was clicked, never what message or text it was clicked next to.
Limits worth knowing while building
- 3 composer buttons and 3 context-menu items per plugin, maximum.
- 10 notifications per minute. A plugin that exceeds this is automatically disabled, not just throttled.
- Registering well past the button/menu-item cap repeatedly also auto-disables a plugin.
- Plugin files are capped at 2MB on install.
Distributing a plugin
There's no store or publishing flow. Share the .js file however you like (a gist, a repo, a Sunflare server's file uploads). Anyone installing it goes through the same native file picker as any other install.
Docs