Developing Plugins
Want to extend FediSuite with your own features? This page explains how a plugin is structured, how to get started in minutes with the scaffold tool, and what possibilities the plugin system offers — understandable even without deep prior knowledge.
On this page
Who is this page for?
This page is for anyone who wants to build their own plugins for FediSuite or understand how the plugin system works from the inside. You don't need to be an experienced developer — the scaffold tool handles most of the groundwork, and the examples on this page show you what goes where.
This page is for you if …
- … you want to develop your own plugin.
- … you want to understand how plugins are structured.
- … you want to customize an existing plugin.
This page is not for you if …
- … you only want to install plugins → Installing Plugins
- … you want to enable/disable plugins → Plugin Management
Prerequisites
You don't need deep programming experience to build a simple plugin. Basic JavaScript knowledge is sufficient for most plugin types.
Plugins are written in JavaScript. You don't need to be an expert — for simple admin pages or widgets it's enough to know what a function is and how to write objects.
Required for the scaffold tool, which automatically sets up the basic structure. Node.js is already installed on most development machines. Check with: node --version
For cloning the plugin repository where you develop your plugin. Also for updates and versioning.
To test your plugin, you need a local or remote FediSuite instance with the plugins/ folder mounted.
Quick Start with the Scaffold
The fastest and recommended way to create your own plugin is the scaffold tool. It lives directly in the FediSuite application and creates a complete, immediately runnable plugin structure with a single command — including plugin.json, server/index.js, language files and a README. You don't start from a blank slate.
The scaffold tool is part of the FediSuite application itself and is therefore available in the app container — not in the plugins directory. Open a shell in the running app container and run the command there in the main directory /app:
# Open a shell in the app container
docker compose exec app sh
# Run the scaffold in the main application directory
npm run plugins:create -- \
--id my-plugin \
--name "My Plugin" \
--author "Your Name" \
--presets admin-page
What do the parameters mean?
--id
The technical ID of your plugin. Only letters, numbers, hyphens and underscores are allowed. Used as a unique key in FediSuite and in the API path (/api/plugins/<id>/). Once set, do not change it.
--name
The human-readable display name that appears in plugin management. Can contain spaces and special characters.
--author
Your name or the name of your organization. Appears in the plugin detail view.
--presets
Which type of plugin to create. Multiple presets can be combined with commas.
Available presets
Each preset represents a different plugin type and sets up the appropriate basic structure. Presets can be combined: --presets app-page,dashboard-widget
admin-page
Creates a new page in the admin area of FediSuite.
app-page
Creates a new page in the main navigation of the app (visible to all users).
dashboard-widget
Adds a widget to the dashboard — e.g. a statistics tile.
composer-extension
Extends the post editor with custom fields, e.g. hashtag suggestions or text transformations.
provider
Connects a new platform (e.g. a social network) to FediSuite — just like the Bluesky plugin.
auth-provider
Enables login via an external platform or a custom authentication system.
insights-provider
Provides tips or analyses to the FediSuite Insights dashboard.
settings
Adds configurable settings for the plugin that can be edited in the admin area.
--presets admin-page. A simple admin page is the most straightforward plugin type and shows you the complete basic structure without needing provider logic or API routes.
Plugin Structure
Every plugin is a simple folder within the plugins/ directory. The folder name matches the plugin ID. FediSuite recognizes a plugin by finding a plugin.json file in that folder.
Minimal (required)
my-plugin/
├── plugin.json ← required
├── server/
│ └── index.js ← required
└── i18n/
├── de.json ← required
├── en.json ← required
└── it.json ← required
With custom web pages (optional)
my-plugin/
├── plugin.json
├── server/
│ └── index.js
├── i18n/
│ ├── de.json
│ ├── en.json
│ └── it.json
└── web/ ← optional
├── manifest.json
├── main.html
└── main.js
plugin.json
The manifest: describes the plugin, its capabilities and permissions. FediSuite reads this file first.
server/index.js
The server-side entry file. This is where you register everything your plugin integrates into FediSuite.
i18n/de.json
German language file. Contains all plugin texts as key-value pairs.
i18n/en.json
English language file. FediSuite falls back to English if the user's language is not available.
i18n/it.json
Italian language file.
web/manifest.json
Optional. Only needed if the plugin embeds its own HTML/JS pages in FediSuite.
The Plugin Manifest (plugin.json)
The plugin.json is the most important file of a plugin. It always lives in the root directory of the plugin folder and tells FediSuite who the plugin is, what it can do and what rights it needs. If it is missing or invalid, the plugin will not be loaded.
A complete example:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"pluginApiVersion": 1,
"description": "A short description of the plugin.",
"author": "Your Name",
"license": "GPL-3.0-or-later",
"capabilities": ["admin.section"],
"requiredPermissions": ["admin.sections"],
"serverEntry": "./server/index.js",
"i18nDir": "./i18n",
"displayNameKey": "meta.name",
"descriptionKey": "meta.description"
}
Required fields
id
string
The stable, technical ID of the plugin. Only letters, numbers, dots, hyphens and underscores are allowed. It is used as the API path (/api/plugins/<id>/...) and i18n namespace (plugin.<id>). Once assigned, do not change it.
name
string
The human-readable display name that appears in plugin management.
version
string
The version number following the Major.Minor.Patch scheme, e.g. "1.0.0".
pluginApiVersion
number
The version of the plugin API this plugin uses. Currently always 1. If the value does not match FediSuite, the plugin will be marked as INCOMPATIBLE and not loaded.
description
string
A short English-language description of what the plugin does.
author
string
Name of the author or organization.
license
string
The license of the plugin, e.g. "GPL-3.0-or-later" or "MIT".
capabilities
array
What the plugin technically does — e.g. ["admin.section"]. Describes the plugin type. Not to be confused with requiredPermissions.
serverEntry
string
Relative path to the JavaScript entry file on the server side. Typically "./server/index.js".
Recommended fields
requiredPermissions
array
Which integration rights the plugin needs in FediSuite — e.g. ["admin.sections"]. Without correctly declared permissions, the boot process intentionally fails. Capabilities and permissions are two different concepts: capabilities describe WHAT the plugin is, permissions determine WHAT it is allowed to do.
i18nDir
string
Path to the directory with language files, e.g. "./i18n".
displayNameKey
string
i18n key for the display name, e.g. "meta.name". FediSuite reads the translated text from the matching language file.
descriptionKey
string
i18n key for the description, e.g. "meta.description".
Optional fields
webEntry
string
Path to the web manifest file, e.g. "./web/manifest.json". Only set this if the plugin embeds its own HTML/JS pages in FediSuite.
Capabilities and Permissions
In plugin.json there are two fields that are easy to confuse: capabilities and requiredPermissions.
capabilities
Describes what the plugin technically is — its type. Example: a plugin with provider is a platform connector.
requiredPermissions
Describes which integration rights the plugin needs. If permissions are missing or incorrect, the plugin start intentionally fails.
Which capability belongs to which permission — and what do they mean?
admin.section
admin.sections
Adds a new page to the admin area of FediSuite.
Opt.: web.runtime, api.routes
app.section
app.sections
Adds a new page to the main navigation of the app, visible to all users.
Opt.: web.runtime, api.routes
dashboard.widget
dashboard.widgets
Displays a widget on the dashboard — e.g. a statistics tile.
composer.extension
composer.extensions
Extends the post editor with custom fields or transformations.
provider
providers
Connects a new platform to FediSuite (just like the Bluesky plugin).
Opt.: web.runtime
auth.provider
auth.providers
Enables login via an external platform or a custom authentication system.
insights.provider
insight.providers
Provides tips or analyses to the Insights dashboard.
capabilities: ["provider"] must also declare requiredPermissions: ["providers"] — otherwise the start fails. The scaffold sets both automatically.
The Entry File: server/index.js
The file server/index.js is the heart of every plugin. FediSuite calls the register(context) function it contains at startup. In this function you tell FediSuite what your plugin adds.
The minimal skeleton always looks the same:
export function register(context) {
// Register everything your plugin integrates into FediSuite here.
// The context parameter is your interface to FediSuite.
}
What is context?
context is your official interface to FediSuite. Through it you register all features of your plugin. Think of it as a toolkit: you grab exactly the methods your plugin type needs.
Plugin information
context.plugin
Contains the plugin's metadata, e.g. context.plugin.plugin_id for the plugin ID.
context.namespace
The i18n namespace of the plugin, e.g. plugin.my-plugin. Used internally for language files.
Pages & UI
registerMarkdownAdminPage()
Simplest way to add an admin page: the content comes from a Markdown string in the language file.
registerMarkdownAppPage()
Like above, but for app pages (visible to all users).
registerAdminSection()
Registers an admin page with custom HTML/JS (via web/manifest.json).
registerAppSection()
Registers an app page with custom HTML/JS.
Widgets & Extensions
registerSimpleDashboardWidget()
Adds a simple statistics widget to the dashboard.
registerDashboardWidget()
Dashboard widget with full control over rendering.
registerSimpleComposerExtension()
Adds fields and transformations to the post editor.
registerComposerExtension()
Composer extension with full control.
Providers
registerSimpleProvider()
Connects a new platform — the easiest entry point for provider plugins.
registerSimpleAuthProvider()
Adds a login provider.
registerSimpleInsightProvider()
Provides tips to the Insights dashboard.
API & Settings
registerApiRoute()
Registers an HTTP endpoint at /api/plugins/<pluginId>/... — called by plugin web pages via the SDK.
getSettings()
Loads all configured settings of the plugin.
getSetting(key, fallback)
Loads a single setting value. The fallback value is returned if the key is not set.
Internationalization
context.createI18nRef(key, fallback)
Creates a reference to a translation key from the language files. Always use this instead of entering texts directly as strings.
context.ref(key, fallback)
Short form of createI18nRef().
Plugin Types and Examples
Here you can see how the most important plugin types are registered in server/index.js — the simplest working example for each.
The simplest plugin type. The page content comes directly from a Markdown text in the language file. No HTML, no JavaScript required.
export function register(context) {
context.registerMarkdownAdminPage({
id: 'main',
titleKey: context.ref('adminSections.main.title', 'My Plugin'),
descriptionKey: context.ref('adminSections.main.description', 'Description'),
markdownKey: context.ref('adminSections.main.markdown', '## Content\nText goes here.'),
});
}
Adds a statistics tile to the dashboard. Value, label and optional trend information are set via the language file.
export function register(context) {
context.registerSimpleDashboardWidget({
id: 'main',
displayNameKey: context.ref('widgets.main.displayName', 'My Widget'),
});
}
Extends the post editor with a custom text field. The transformPost function is called before the post is published — here you can modify the content.
export function register(context) {
context.registerSimpleComposerExtension({
id: 'main',
displayNameKey: context.ref('composer.main.displayName', 'My Extension'),
fields: [
{ key: 'suffix', type: 'text', label: 'Appendix' },
],
transformPost: async ({ post, data }) => ({
content: `${String(post.content || '')}${String(data?.suffix || '')}`,
}),
});
}
Connects a new platform to FediSuite. beginConnection starts the connection flow and returns a redirect URL. handleCallback processes the response and returns account data.
export function register(context) {
context.registerSimpleProvider({
id: 'my-network',
displayNameKey: context.ref('providers.main.displayName', 'My Network'),
descriptionKey: context.ref('providers.main.description', 'Connects My Network.'),
beginConnection: async ({ req }) => ({
redirect_url: `${req.protocol}://${req.get('host')}/api/plugins/my-plugin/callback`,
}),
handleCallback: async () => ({
user_id: 1,
account: {
instance_url: 'https://example.com',
username: 'demo',
display_name: 'Demo Account',
stats_followers: 0,
stats_following: 0,
stats_statuses: 0,
max_characters: 500,
max_media_attachments: 4,
},
}),
});
}
Provides tips to the Insights dashboard. Each tip has an ID, a title and a text.
export function register(context) {
context.registerSimpleInsightProvider({
id: 'my-insights',
displayNameKey: context.ref('insights.main.displayName', 'My Insights'),
tips: [
{ id: 'tip-1', title: 'First Tip', text: 'This tip comes from the plugin.' },
],
});
}
Exposes a custom HTTP endpoint at /api/plugins/<pluginId>/.... Typically called by plugin web pages via the SDK. auth: 'user' means the user must be logged in; auth: 'admin' requires admin rights.
export function register(context) {
context.registerApiRoute({
method: 'get',
path: '/status', // accessible at /api/plugins/<id>/status
auth: 'user', // 'user', 'admin', or empty for public
summary: 'Returns the plugin status.',
handler: async ({ req, res }) => {
res.json({ plugin_id: context.plugin.plugin_id, status: 'ok' });
},
});
}
Custom Web Pages (web/)
If your plugin needs a fully interactive page with custom HTML and JavaScript elements, you can provide custom web files. FediSuite embeds these directly in the app — you don't need a separate server.
The web/ directory is optional and only needed if you really want custom HTML/JS. For simple text or Markdown content you don't need it.
web/manifest.json
This file describes which HTML files belong to which registered sections. The sectionId must match exactly the ID you assigned in server/index.js.
For admin pages
{
"frontendApiVersion": 1,
"adminPages": [
{
"sectionId": "main",
"entry": "admin-main.html"
}
]
}
For app pages
{
"frontendApiVersion": 1,
"appPages": [
{
"sectionId": "main",
"entry": "app-main.html"
}
]
}
sectionId in web/manifest.json must be identical to the id in registerAdminSection() or registerAppSection(). Any difference — even just in capitalization — will cause the web page to not load.
The Plugin Web SDK
For communication between the HTML page and FediSuite, a ready-made SDK is available. It handles authentication, API calls and embedding in the app — you don't need to manage tokens or HTTP headers yourself.
Include the SDK in your HTML file:
<script src="/plugin-sdk/fedisuite-plugin-web.js"></script>
Afterwards window.FediSuitePluginWeb is available. A typical basic skeleton:
async function boot() {
const plugin = window.FediSuitePluginWeb;
// Wait until FediSuite has initialized the page (always call this first!)
const context = await plugin.ready();
// context contains: pluginId, sectionId, sectionKind, language
// Call the plugin API (endpoint from registerApiRoute)
const result = await plugin.get('/status');
console.log(result);
}
boot();
Available SDK methods
ready()
Waits for initialization by FediSuite. Returns context data (pluginId, sectionId, language). Always call this first, before any API calls.
getContext()
Returns the already initialized context immediately. Only use after ready().
get(path)
Sends a GET request to /api/plugins/<pluginId>/<path>.
post(path, body)
Sends a POST request. body is a JavaScript object and is transmitted as JSON.
put(path, body)
Sends a PUT request.
patch(path, body)
Sends a PATCH request.
delete(path, body)
Sends a DELETE request.
autoResize()
Automatically adjusts the plugin page to its content height. Returns a cleanup function.
Internationalization (i18n)
FediSuite supports multiple languages. To keep your plugin consistent with the rest of the app, you should store all texts — titles, descriptions, content — in language files rather than hardcoding them. The directory for this is i18n/.
A typical language file looks like this:
{
"meta": {
"name": "My Plugin",
"description": "Short description of my plugin."
},
"adminSections": {
"main": {
"title": "My Plugin",
"description": "What this plugin does.",
"markdown": "## Welcome\n\nContent goes here."
}
}
}
In the code, instead of using the text directly, you use a reference to the key:
// ✓ Correct: use a language reference
titleKey: context.ref('adminSections.main.title', 'My Plugin')
// ✗ Wrong: text entered directly
titleKey: 'My Plugin'
context.ref('adminSections.main.title', 'My Plugin') — if the key is not found in any language file, FediSuite shows the fallback text. Useful to always see something readable during development.
Common Errors & Debugging
Most problems when developing plugins fall into four categories. Here are the most common causes and how to quickly find them.
Plugin is not recognized
- Is plugin.json in the root directory of the plugin folder (not in a subfolder)?
- Is the plugins/ folder correctly mounted as a volume in docker-compose.yml?
- Were the containers restarted after a change?
ls -la plugins/my-plugin/
docker compose config | grep plugins
docker compose up -d
Plugin does not boot (status ERROR or INCOMPATIBLE)
- Is pluginApiVersion in plugin.json correct? The value must currently be 1.
- Does the file that serverEntry points to exist?
- Are both capabilities and requiredPermissions set correctly and completely?
- Does server/index.js contain an exported function named register?
docker compose logs app | grep -i plugin
docker compose logs app | grep -i error
Page or widget does not appear in the app
- Was the section registered with the correct method (registerAdminSection vs. registerAppSection)?
- Is the correct permission entered in requiredPermissions?
- Is the plugin activated in plugin management?
docker compose logs app
Plugin web page stays blank or does not load
- Is webEntry set in plugin.json?
- Is web/manifest.json valid and syntactically correct (valid JSON)?
- Does the sectionId in web/manifest.json exactly match the one in registerAdminSection()/registerAppSection()?
- Is the SDK included: <script src="/plugin-sdk/fedisuite-plugin-web.js">?
- Is await window.FediSuitePluginWeb.ready() called before all API calls?
- Are asset paths relative to the web/ directory (./main.js, ./style.css)?
docker compose logs app | grep -i plugin
← Back
Plugin Management
Next →
FAQ
Coming soon