Plugin System

Overview#

Flare CMS includes a full plugin system that lets you extend every part of the CMS: routes, middleware, database models, admin pages, and content lifecycle events. Plugins are plain TypeScript objects that conform to the Plugin interface, managed by a central PluginManager and connected through an event-driven hook system.

Architecture#

The plugin system has four core components:

ComponentRole
PluginManagerCentral orchestrator -- installs, activates, and uninstalls plugins
PluginRegistryTracks registered plugins, their status, configs, and dependency order
HookSystemEvent bus for inter-plugin communication and lifecycle events
PluginValidatorValidates plugin definitions and dependency graphs before registration

How a plugin loads#

  1. PluginManager.install(plugin) validates the plugin definition
  2. The plugin is registered in the PluginRegistry
  3. A scoped hook system is created for the plugin (isolated cleanup on uninstall)
  4. Plugin extensions are registered (routes, middleware, hooks, models, services)
  5. The plugin's install() lifecycle callback runs
  6. The plugin:install hook fires globally
  7. The plugin's activate() callback runs
  8. The registry marks the plugin as active

Plugin Interface#

Every plugin must have a name and version. All other fields are optional extension points:

interface Plugin {
  name: string
  version: string
  description?: string
  author?: { name: string; email?: string; url?: string }
  dependencies?: string[]
  compatibility?: string
  license?: string
 
  // Extension points
  routes?: PluginRoutes[]
  middleware?: PluginMiddleware[]
  models?: PluginModel[]
  services?: PluginService[]
  adminPages?: PluginAdminPage[]
  adminComponents?: PluginComponent[]
  menuItems?: PluginMenuItem[]
  hooks?: PluginHook[]
 
  // Lifecycle callbacks
  install?: (context: PluginContext) => Promise<void>
  uninstall?: (context: PluginContext) => Promise<void>
  activate?: (context: PluginContext) => Promise<void>
  deactivate?: (context: PluginContext) => Promise<void>
  configure?: (config: PluginConfig) => Promise<void>
}

Hook System#

Hooks are the primary communication mechanism between plugins and core. The hook system is priority-ordered (lower number = earlier execution, default priority is 10) and supports cancellation.

Standard Hook Names#

Flare CMS defines these built-in hooks in the HOOKS constant:

Application lifecycle:

HookConstantFires when
app:initHOOKS.APP_INITPlugin system initializes
app:readyHOOKS.APP_READYApplication is ready to serve
app:shutdownHOOKS.APP_SHUTDOWNApplication is shutting down

Request lifecycle:

HookConstantFires when
request:startHOOKS.REQUEST_STARTIncoming request begins processing
request:endHOOKS.REQUEST_ENDResponse is about to be sent
request:errorHOOKS.REQUEST_ERRORUnhandled error during request

Authentication:

HookConstantFires when
auth:loginHOOKS.AUTH_LOGINUser logs in
auth:logoutHOOKS.AUTH_LOGOUTUser logs out
auth:registerHOOKS.AUTH_REGISTERNew user registers
user:loginHOOKS.USER_LOGINUser login event
user:logoutHOOKS.USER_LOGOUTUser logout event

Content lifecycle:

HookConstantFires when
content:createHOOKS.CONTENT_CREATENew content is created
content:updateHOOKS.CONTENT_UPDATEContent is updated
content:deleteHOOKS.CONTENT_DELETEContent is deleted
content:publishHOOKS.CONTENT_PUBLISHContent is published
content:saveHOOKS.CONTENT_SAVEContent is saved (create or update)

Media lifecycle:

HookConstantFires when
media:uploadHOOKS.MEDIA_UPLOADFile is uploaded
media:deleteHOOKS.MEDIA_DELETEMedia is deleted
media:transformHOOKS.MEDIA_TRANSFORMImage transformation requested

Plugin lifecycle:

HookConstantFires when
plugin:installHOOKS.PLUGIN_INSTALLPlugin is installed
plugin:uninstallHOOKS.PLUGIN_UNINSTALLPlugin is uninstalled
plugin:activateHOOKS.PLUGIN_ACTIVATEPlugin is activated
plugin:deactivateHOOKS.PLUGIN_DEACTIVATEPlugin is deactivated

Admin interface:

HookConstantFires when
admin:menu:renderHOOKS.ADMIN_MENU_RENDERAdmin menu is rendering
admin:page:renderHOOKS.ADMIN_PAGE_RENDERAdmin page is rendering

Database:

HookConstantFires when
db:migrateHOOKS.DB_MIGRATEDatabase migration runs
db:seedHOOKS.DB_SEEDDatabase seeding runs

Hook Execution#

Handlers run in priority order. Each handler receives the current data and a HookContext:

const handler = async (data: any, context: HookContext) => {
  // Modify and return data to pass to next handler
  return { ...data, processed: true }
 
  // Or cancel the hook chain
  context.cancel()
  return data
}

The hook system prevents infinite recursion -- if a hook triggers itself, the recursive call is skipped with a console warning.

Scoped Hooks#

Each plugin gets a ScopedHookSystem that tracks which hooks it registered. When a plugin is uninstalled, unregisterAll() cleanly removes all its hooks without affecting other plugins.

Plugin Context#

When lifecycle callbacks run, they receive a PluginContext with access to CMS internals:

interface PluginContext {
  db: D1Database
  kv: KVNamespace
  r2?: R2Bucket
  config: PluginConfig
  services: {
    auth: AuthService
    content: ContentService
    media: MediaService
  }
  hooks: ScopedHookSystem
  logger: PluginLogger
  app?: Hono  // Main app instance for registering routes
}

The logger provides namespaced logging (debug, info, warn, error) that prefixes messages with [Plugin:name].

Extension Points#

Routes#

Plugins can add API routes that get mounted on the main Hono app:

{
  routes: [{
    path: '/api/my-feature',
    handler: myHonoApp,
    requiresAuth: true,
    roles: ['admin']
  }]
}

Middleware#

Plugin middleware is sorted by priority and can be applied globally or to specific routes:

{
  middleware: [{
    name: 'my-middleware',
    handler: myMiddlewareHandler,
    priority: 5,
    global: true
  }]
}

Database Models#

Plugins can declare database models with Zod schemas and SQL migrations:

{
  models: [{
    name: 'MyModel',
    tableName: 'my_table',
    schema: myZodSchema,
    migrations: [createTableSQL],
    extendsContent: true
  }]
}

Admin Pages and Menu Items#

Plugins can add pages to the admin UI and items to the admin menu:

{
  adminPages: [{
    path: '/admin/my-feature',
    title: 'My Feature',
    component: 'MyFeatureComponent',
    icon: 'sparkles'
  }],
  menuItems: [{
    label: 'My Feature',
    path: '/admin/my-feature',
    icon: 'sparkles',
    order: 50
  }]
}

Dependency Resolution#

The PluginRegistry resolves plugin load order based on declared dependencies. If plugin B depends on plugin A, A is always activated first. Circular dependencies are detected and reported as errors during validation.

// Plugin B declares it needs core-auth
{
  name: 'my-plugin',
  dependencies: ['core-auth']
}

Next Steps#

  • See Core Plugins for the built-in plugins that ship with Flare CMS
  • See Building Plugins for a step-by-step guide to creating your own plugin