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
| Event | Scope | Description |
|---|---|---|
downloadableFile.created | Server | New downloadable file uploaded |
downloadableFile.updated | Server | Downloadable file replaced or modified |
downloadableFile.downloaded | Server | Download request needs a fresh check |
comment.created | Server or channel | New comment posted |
discussionChannel.created | Channel | Discussion 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 Admin → Plugins → Pipelines for server policy and Channel Settings → Pipelines 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
| Field | Required | Description |
|---|---|---|
name | Yes | Unique plugin identifier |
version | Yes | Semantic version |
description | Yes | What the plugin does |
author | No | Plugin author |
events | Yes | Events the plugin handles |
secrets | No | Required 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
| Scope | Description |
|---|---|
server | Single value for entire server |
channel | Can be different per channel |
Setting Secrets
Via the admin panel:
- Go to Admin Settings → Plugins
- Select the plugin
- Click Set Secret
- Enter the secret value
- 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
- Add registry URL to
ServerConfig.pluginRegistries - Go to Admin Settings → Plugins
- Click Refresh Plugins
- Select a plugin
- Click Install
- Configure secrets if required
- Enable the plugin
Plugin Versioning
Plugins support versions:
- Install specific versions
- Update to newer versions
- Roll back if needed
Enabling Plugins
Server-Level
- Go to Admin Settings → Plugins
- Find the installed plugin
- Toggle Enabled
- Configure pipeline placement
Channel-Level
- Install and enable the plugin at server level.
- Go to Channel Settings → Pipelines.
- 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.