Plugins

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.

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 …

Prerequisites

You don't need deep programming experience to build a simple plugin. Basic JavaScript knowledge is sufficient for most plugin types.

Basic JavaScript knowledge Recommended

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.

Node.js and npm Required for Scaffold

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

Git Required

For cloning the plugin repository where you develop your plugin. Also for updates and versioning.

Running FediSuite installation Required for testing

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:

bash
# 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.

Recommendation for beginners: Start with --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:

plugin.json
{
  "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?

Type capability permission (required) Description
Admin Page admin.section admin.sections

Adds a new page to the admin area of FediSuite.

Opt.: web.runtime, api.routes

App Page 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.widget dashboard.widgets

Displays a widget on the dashboard — e.g. a statistics tile.

Composer Extension composer.extension composer.extensions

Extends the post editor with custom fields or transformations.

Provider / Connector provider providers

Connects a new platform to FediSuite (just like the Bluesky plugin).

Opt.: web.runtime

Auth Provider auth.provider auth.providers

Enables login via an external platform or a custom authentication system.

Insight Provider insights.provider insight.providers

Provides tips or analyses to the Insights dashboard.

Important: Both capabilities and permissions must be set correctly. A plugin with 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:

server/index.js
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.

Admin Page admin-page

The simplest plugin type. The page content comes directly from a Markdown text in the language file. No HTML, no JavaScript required.

server/index.js
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.'),
  });
}
Dashboard Widget dashboard-widget

Adds a statistics tile to the dashboard. Value, label and optional trend information are set via the language file.

server/index.js
export function register(context) {
  context.registerSimpleDashboardWidget({
    id:           'main',
    displayNameKey: context.ref('widgets.main.displayName', 'My Widget'),
  });
}
Composer Extension composer-extension

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.

server/index.js
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 || '')}`,
    }),
  });
}
Provider / Platform Connector provider

Connects a new platform to FediSuite. beginConnection starts the connection flow and returns a redirect URL. handleCallback processes the response and returns account data.

server/index.js
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,
      },
    }),
  });
}
Insight Provider insights-provider

Provides tips to the Insights dashboard. Each tip has an ID, a title and a text.

server/index.js
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.' },
    ],
  });
}
Plugin API Route registerApiRoute

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.

server/index.js
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

web/manifest.json
{
  "frontendApiVersion": 1,
  "adminPages": [
    {
      "sectionId": "main",
      "entry": "admin-main.html"
    }
  ]
}

For app pages

web/manifest.json
{
  "frontendApiVersion": 1,
  "appPages": [
    {
      "sectionId": "main",
      "entry": "app-main.html"
    }
  ]
}
Important: The 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:

html
<script src="/plugin-sdk/fedisuite-plugin-web.js"></script>

Afterwards window.FediSuitePluginWeb is available. A typical basic skeleton:

javascript (in your HTML file)
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:

i18n/en.json
{
  "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:

server/index.js
// ✓ Correct: use a language reference
titleKey: context.ref('adminSections.main.title', 'My Plugin')

// ✗ Wrong: text entered directly
titleKey: 'My Plugin'
The second parameter is the fallback. 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?
Diagnostic commands
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?
Diagnostic commands
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?
Diagnostic commands
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)?
Diagnostic commands
docker compose logs app | grep -i plugin
Complete reference: The official plugin authoring documentation can be found directly in the plugin repository at PLUGIN-AUTHORING.md on Codeberg ↗.

← Back

Plugin Management

Next →

FAQ

Coming soon