January 9, 2026
Plugin Events
The Rox plugin system provides an event system for hooking into application lifecycle events. This document explains the available events and how to use them.
EventBus Overview
EventBus provides a pub/sub mechanism for plugins to subscribe to application events.
Event Types
Rox has two types of events:
| Type | Description | Execution |
|---|---|---|
| before | Fired before an operation. Can cancel or modify | Sequential |
| after | Fired after an operation. Notification only (fire-and-forget) | Parallel |
Event Naming Convention
Event names follow the format {resource}:{timing}{Action}:
note:beforeCreate- Before note creationnote:afterCreate- After note creationuser:beforeRegister- Before user registrationuser:afterRegister- After user registration
"before" Events
"before" events can cancel or modify operations. Handlers are executed sequentially.
How to Subscribe
events.onBefore("note:beforeCreate", (data) => {
// Process
return {};
});
Return Value Options
"before" event handlers must return one of the following:
1. Continue (no changes)
events.onBefore("note:beforeCreate", (data) => {
// Continue without changes
return {};
});
2. Cancel the operation
events.onBefore("note:beforeCreate", (data) => {
if (data.content.includes("blocked word")) {
return {
cancel: true,
reason: "Content contains blocked words"
};
}
return {};
});
3. Modify the data
events.onBefore("note:beforeCreate", (data) => {
// Sanitize content
return {
modified: {
...data,
content: sanitize(data.content)
}
};
});
Available "before" Events
note:beforeCreate
Fired before a note is created.
interface NoteBeforeCreateData {
/** Note content text */
content: string;
/** User ID of the author */
userId: string;
/** Content warning (optional) */
cw?: string | null;
/** Visibility level */
visibility?: "public" | "home" | "followers" | "specified";
/** Local-only flag */
localOnly?: boolean;
}
Example: Content Filtering
events.onBefore("note:beforeCreate", ({ content, cw }) => {
const blockedWords = ["spam", "advertisement"];
if (blockedWords.some(word => content.includes(word))) {
return { cancel: true, reason: "Content contains blocked words" };
}
// Add content warning for NSFW content
if (content.includes("NSFW") && !cw) {
return { modified: { content, cw: "Sensitive content" } };
}
return {};
});
note:beforeDelete
Fired before a note is deleted.
interface NoteBeforeDeleteData {
/** ID of the note to delete */
noteId: string;
/** User ID performing the deletion */
userId: string;
}
Example: Deletion Protection
events.onBefore("note:beforeDelete", async ({ noteId, userId }) => {
const protectedNotes = await config.get<string[]>("protectedNotes") ?? [];
if (protectedNotes.includes(noteId)) {
return { cancel: true, reason: "This note is protected" };
}
return {};
});
user:beforeRegister
Fired before a user is registered.
interface UserBeforeRegisterData {
/** Username */
username: string;
/** Email address (optional) */
email?: string | null;
}
Example: Username Validation
events.onBefore("user:beforeRegister", ({ username }) => {
const reservedNames = ["admin", "root", "system"];
if (reservedNames.includes(username.toLowerCase())) {
return { cancel: true, reason: "This username is reserved" };
}
return {};
});
"after" Events
"after" events are notification only and cannot cancel operations. Handlers are executed in parallel.
How to Subscribe
events.on("note:afterCreate", ({ note }) => {
// Process
});
Available "after" Events
note:afterCreate
Fired after a note is successfully created.
interface NoteAfterCreateData {
/** The created note */
note: Note;
}
Example: Activity Logging
events.on("note:afterCreate", ({ note }) => {
logger.info({
noteId: note.id,
userId: note.userId,
visibility: note.visibility
}, "New note created");
});
note:afterDelete
Fired after a note is successfully deleted.
interface NoteAfterDeleteData {
/** ID of the deleted note */
noteId: string;
/** User ID who performed the deletion */
userId: string;
}
Example: Deletion Logging
events.on("note:afterDelete", ({ noteId, userId }) => {
logger.info({ noteId, userId }, "Note deleted");
});
user:afterRegister
Fired after a user is successfully registered.
interface UserAfterRegisterData {
/** ID of the registered user */
userId: string;
/** Username */
username: string;
}
Example: Welcome Message
events.on("user:afterRegister", async ({ userId, username }) => {
logger.info({ userId, username }, "New user registered");
// Send welcome notification, etc.
await sendWelcomeNotification(userId);
});
Unsubscribing
Both events.on and events.onBefore return an unsubscribe function.
onLoad({ events }) {
const unsubscribe = events.on("note:afterCreate", ({ note }) => {
console.log("Note created:", note.id);
});
// Unsubscribe when needed
unsubscribe();
}
NOTE
When a plugin is unloaded, its event subscriptions are automatically cleaned up.
Error Handling
"after" Events
Errors in "after" event handlers don't affect other handlers. Errors are logged but not thrown.
events.on("note:afterCreate", ({ note }) => {
throw new Error("Something went wrong"); // Doesn't affect other handlers
});
"before" Events
Errors in "before" event handlers propagate and prevent subsequent handlers from running.
events.onBefore("note:beforeCreate", (data) => {
throw new Error("Validation failed"); // Operation is aborted
});
Practical Examples
Content Moderation Plugin
import type { RoxPlugin } from "@rox/backend/plugins";
const moderationPlugin: RoxPlugin = {
id: "moderation",
name: "Content Moderation",
version: "1.0.0",
async onLoad({ events, config, logger }) {
const blockedWords = await config.get<string[]>("blockedWords") ?? [];
const blockedPatterns = await config.get<string[]>("blockedPatterns") ?? [];
// Content filtering
events.onBefore("note:beforeCreate", ({ content }) => {
// Check blocked words
for (const word of blockedWords) {
if (content.toLowerCase().includes(word.toLowerCase())) {
logger.warn({ word }, "Blocked word detected");
return { cancel: true, reason: `Blocked word "${word}" detected` };
}
}
// Pattern matching
for (const pattern of blockedPatterns) {
if (new RegExp(pattern, "i").test(content)) {
logger.warn({ pattern }, "Blocked pattern detected");
return { cancel: true, reason: "Blocked pattern detected" };
}
}
return {};
});
// Moderation logging
events.on("note:afterCreate", ({ note }) => {
logger.debug({ noteId: note.id }, "Note passed moderation");
});
},
};
export default moderationPlugin;
Analytics Plugin
import type { RoxPlugin } from "@rox/backend/plugins";
const analyticsPlugin: RoxPlugin = {
id: "analytics",
name: "Analytics Plugin",
version: "1.0.0",
async onLoad({ events, config, logger }) {
let noteCount = 0;
let userCount = 0;
events.on("note:afterCreate", ({ note }) => {
noteCount++;
logger.info({
total: noteCount,
noteId: note.id
}, "Note creation stats");
});
events.on("note:afterDelete", ({ noteId }) => {
noteCount--;
logger.info({ total: noteCount, noteId }, "Note deletion stats");
});
events.on("user:afterRegister", ({ userId, username }) => {
userCount++;
logger.info({
total: userCount,
userId,
username
}, "User registration stats");
});
},
};
export default analyticsPlugin;
Event Summary
| Event | Type | Description |
|---|---|---|
note:beforeCreate |
before | Before note creation |
note:afterCreate |
after | After note creation |
note:beforeDelete |
before | Before note deletion |
note:afterDelete |
after | After note deletion |
user:beforeRegister |
before | Before user registration |
user:afterRegister |
after | After user registration |
Related Documentation
- Plugin Getting Started - Introductory guide
- Plugin Architecture - Architecture details
- Plugin Manifest - plugin.json reference