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 creation
  • note:afterCreate - After note creation
  • user:beforeRegister - Before user registration
  • user: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