Skip to main content

Custom Hooks (Plugins)

Multiforum supports a plugin system that allows extending functionality with custom code. Plugins can react to events and automate workflows.

Plugin System Overview

Plugins in Multiforum:

  • Run in response to events (e.g., discussion created, comment posted)
  • Are configured via pipelines
  • Can have server-level or channel-level scope
  • Support secrets for API keys and credentials

Plugin Architecture

┌─────────────────┐     Event     ┌─────────────────┐
│ Multiforum │ ────────────▶ │ Plugin Engine │
│ (Backend) │ │ │
└─────────────────┘ └────────┬────────┘

┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Plugin A│ │Plugin B│ │Plugin C│
└────────┘ └────────┘ └────────┘

Events and Pipelines

Available Events

EventScopeDescription
downloadableFile.createdServerNew downloadable file uploaded
downloadableFile.updatedServerDownloadable file replaced or modified
downloadableFile.downloadedServerDownload request needs a fresh check
comment.createdServer or channelNew comment posted
discussionChannel.createdChannelDiscussion submitted to a channel

Configuring Pipelines

Pipelines define which plugins run for each event. Installing and enabling a plugin makes it selectable, but does not schedule it automatically.

Server-level example:

pipelines:
- event: downloadableFile.created
applicability: NEW_FILES_ONLY
stopOnFirstFailure: true
steps:
- plugin: security-attachment-scan
condition: ALWAYS
continueOnError: false

Channel-level example:

pipelines:
- event: discussionChannel.created
stopOnFirstFailure: true
steps:
- plugin: auto-labeler
condition: ALWAYS

Use AdminPluginsPipelines for server policy and Channel SettingsPipelines for channel automation. See Plugin pipelines for rollout policies, retry behavior, public history, and quarantine operations.

Plugin Manifest

Plugins are defined with a manifest file:

{
"name": "my-plugin",
"version": "1.0.0",
"description": "Description of what the plugin does",
"author": "Your Name",
"events": ["discussionChannel.created", "comment.created"],
"secrets": [
{
"key": "API_KEY",
"description": "External service API key",
"scope": "server"
}
]
}

Manifest Fields

FieldRequiredDescription
nameYesUnique plugin identifier
versionYesSemantic version
descriptionYesWhat the plugin does
authorNoPlugin author
eventsYesEvents the plugin handles
secretsNoRequired secrets configuration

Plugin Secrets

Plugins can require secrets (API keys, tokens, etc.):

Secret Configuration

{
"secrets": [
{
"key": "OPENAI_API_KEY",
"description": "OpenAI API key for content analysis",
"scope": "server"
},
{
"key": "WEBHOOK_URL",
"description": "Webhook URL for notifications",
"scope": "channel"
}
]
}

Secret Scopes

ScopeDescription
serverSingle value for entire server
channelCan be different per channel

Setting Secrets

Via the admin panel:

  1. Go to Admin SettingsPlugins
  2. Select the plugin
  3. Click Set Secret
  4. Enter the secret value
  5. Save

Secrets are encrypted at rest using PLUGIN_SECRET_ENCRYPTION_KEY.

Plugin Code Structure

// Example plugin handler
export async function handleDiscussionCreated(event, context) {
const { discussion, channel, author } = event;
const { secrets, ogm } = context;

// Access secrets
const apiKey = secrets.API_KEY;

// Perform plugin logic
const result = await analyzeContent(discussion.body, apiKey);

// Optionally return data or take actions
return { analysis: result };
}

Event Payload

interface DiscussionCreatedEvent {
discussion: {
id: string;
title: string;
body: string;
createdAt: Date;
};
channel: {
uniqueName: string;
displayName: string;
};
author: {
username: string;
};
}

Context Object

interface PluginContext {
secrets: Record<string, string>; // Plugin secrets
ogm: OGM; // Database access
serverName: string; // Current server
channelUniqueName?: string; // Channel (if applicable)
}

Installing Plugins

From Registry

  1. Add registry URL to ServerConfig.pluginRegistries
  2. Go to Admin SettingsPlugins
  3. Click Refresh Plugins
  4. Select a plugin
  5. Click Install
  6. Configure secrets if required
  7. Enable the plugin

Plugin Versioning

Plugins support versions:

  • Install specific versions
  • Update to newer versions
  • Roll back if needed

Enabling Plugins

Server-Level

  1. Go to Admin SettingsPlugins
  2. Find the installed plugin
  3. Toggle Enabled
  4. Configure pipeline placement

Channel-Level

  1. Install and enable the plugin at server level.
  2. Go to Channel SettingsPipelines.
  3. Add the approved plugin to an explicit channel event pipeline.

Channel owners can select only server-enabled plugins. They cannot disable a server pipeline or opt out of a server-required security check.

Plugin Execution

Viewing Plugin Runs

For downloads, open the download's public Pipelines tab. It shows applicable checks, attempt history, statuses, and intentionally public diagnostics. Authorized administrators can inspect internal operational logs from plugin administration.

Each run shows:

  • Event that triggered it
  • Input data
  • Output/result
  • Duration
  • Success/failure

Error Handling

If a plugin fails:

  • the attempt and job receive a terminal status;
  • safe diagnostics appear publicly when the plugin emits them;
  • internal errors remain restricted to authorized administrators;
  • required security failures keep the file quarantined;
  • eligible uploaders and moderators can retry the latest failed attempt.

Example Use Cases

Spam Detection

export async function handleCommentCreated(event, context) {
const { comment } = event;
const isSpam = await checkForSpam(comment.text);

if (isSpam) {
// Create moderation issue
await context.ogm.model("Issue").create({
title: "Possible spam detected",
body: `Comment ${comment.id} flagged as potential spam`,
relatedCommentId: comment.id
});
}
}

Auto-Tagging

export async function handleDiscussionCreated(event, context) {
const { discussion } = event;
const suggestedTags = await analyzeTags(discussion.title, discussion.body);

// Could notify author or auto-apply tags
return { suggestedTags };
}

Webhook Notifications

export async function handleDiscussionCreated(event, context) {
const { discussion, channel, author } = event;
const webhookUrl = context.secrets.WEBHOOK_URL;

await fetch(webhookUrl, {
method: "POST",
body: JSON.stringify({
text: `New discussion in ${channel.displayName}: "${discussion.title}" by ${author.username}`
})
});
}

Best Practices

Plugin Development

  • Keep plugins focused (single responsibility)
  • Handle errors gracefully
  • Log important actions
  • Respect rate limits of external services
  • Test thoroughly before deploying

Security

  • Never log secrets
  • Validate input data
  • Use HTTPS for external calls
  • Limit plugin permissions

Performance

  • Avoid blocking operations
  • Use async/await properly
  • Consider timeouts
  • Don't overload pipelines

Current Limitations

  • Pipelines are limited to the defined server and channel events.
  • There is no in-app UI for authoring plugin source code.
  • Plugin releases and manifests must be hosted externally.
  • Channel owners can configure only plugins approved and enabled by the server.