Menu

Plugin Architecture & Lifecycle

Relevant source files

Purpose and Scope

This document explains the hot-loadable plugin system that provides extensibility to the FastAPI Best Architecture without modifying core code. It covers plugin discovery, configuration validation, router injection mechanisms, dependency management, runtime status checking, and lifecycle operations including installation and uninstallation.

For information about specific built-in plugins (OAuth2, Dict, System Config, Code Generator, etc.), see System Plugins Overview. For OAuth2 plugin implementation details, see OAuth2 Plugin. For Dictionary plugin implementation, see Dictionary Plugin.


Plugin Types and Classification

The system supports two distinct plugin types that differ in how they integrate with the application's routing structure:

Extend-Level Plugins

Extend-level plugins inject routes into existing routers within the application. They augment functionality of established modules like /admin without creating new top-level routes.

Configuration Requirements:

  • Must define [api] section in plugin.toml with route configurations
  • Must specify app.extend indicating target app module (e.g., "admin")
  • Routes are injected into corresponding paths in the target app's API structure

Examples: RBAC/Casbin Plugin, Dict Plugin (extends /admin/sys)

Application-Level Plugins

Application-level plugins create new top-level routes that function as independent modules with their own API namespace.

Configuration Requirements:

  • Must define app.router list specifying router object names
  • No [api] section needed
  • Routes are included directly in the main application router

Examples: OAuth2 Plugin (/oauth2/*), System Config Plugin, Code Generator Plugin, Email Plugin

Plugin Type Determination Diagram

Sources: backend/plugin/tools.py115-173


Plugin Discovery and Configuration

Directory Structure

Plugins must follow a specific directory structure under PLUGIN_DIR:

backend/plugin/
├── plugin_name/
│   ├── __init__.py          # Required: Python package marker
│   ├── plugin.toml          # Required: Plugin metadata and configuration
│   ├── README.md            # Required: Plugin documentation
│   ├── requirements.txt     # Optional: Python dependencies
│   ├── api/                 # API routes (extend-level) or router module (app-level)
│   ├── model.py             # Optional: SQLAlchemy models
│   ├── service/             # Optional: Business logic
│   ├── crud/                # Optional: Database operations
│   └── sql/                 # Optional: Database initialization scripts
│       ├── mysql/
│       │   ├── init.sql
│       │   └── init_snowflake.sql
│       └── postgresql/
│           ├── init.sql
│           └── init_snowflake.sql

plugin.toml Configuration Format

The plugin.toml file uses TOML format and must contain specific sections:

Required Fields:

SectionFieldTypeDescription
[plugin]summarystringShort plugin description
[plugin]versionstringSemantic version (e.g., "1.0.0")
[plugin]descriptionstringDetailed description
[plugin]authorstringPlugin author name

Extend-Level Plugin Additional Fields:

SectionFieldTypeDescription
[app]extendstringTarget app module name (e.g., "admin")
[api]{filename}objectPer-API-file routing configuration
[api.{filename}]prefixstringURL prefix for routes (e.g., "/casbin")
[api.{filename}]tagsstringOpenAPI tag for grouping

Application-Level Plugin Additional Fields:

SectionFieldTypeDescription
[app]routerlistRouter object names to include (e.g., ["router_v1"])

Plugin Discovery Process

Discovery Flow Diagram

The get_plugins() function scans PLUGIN_DIR and returns a list of valid plugin package names:

Configuration Validation:

The parse_plugin_config() function validates each plugin's configuration:

  1. Required fields check: Ensures summary, version, description, author exist
  2. Type-specific validation:
    • Extend-level: Must have [api] section and app.extend
    • App-level: Must have app.router list
  3. Raises PluginConfigError if validation fails

Sources: backend/plugin/tools.py40-55 backend/plugin/tools.py100-112 backend/plugin/tools.py115-173


Router Injection Mechanisms

Router injection occurs during application startup via build_final_router(), which orchestrates the injection of both extend-level and app-level plugins into the routing tree.

Extend-Level Plugin Injection

Extend-Level Injection Flow

Key Steps:

  1. Validate API Directory: Checks that backend/plugin/{plugin_name}/api/ exists
  2. Walk Directory Tree: Recursively processes all .py files (excluding __init__.py)
  3. Parse Configuration: Extracts prefix and tags from plugin.toml[api][filename]
  4. Import Plugin Module: Uses import_module_cached() to load the plugin's router module
  5. Build Target Path: Constructs target module path by mapping plugin API structure to app structure
    • Plugin: backend/plugin/{name}/api/v1/casbin.py
    • Target: backend/app/admin/api/v1/casbin.py (if app.extend = "admin")
  6. Inject Router: Calls target_router.include_router() with PluginStatusChecker dependency

Example from Dict Plugin:

Configuration in plugin.toml:

Injection result:

  • backend/plugin/dict/api/dict_data.py router → injected into backend/app/admin/api/v1/sys/ with prefix /dict-data
  • backend/plugin/dict/api/dict_type.py router → injected into backend/app/admin/api/v1/sys/ with prefix /dict-types

Sources: backend/plugin/tools.py176-233

Application-Level Plugin Injection

Application-Level Injection Flow

Key Steps:

  1. Import Router Module: Loads backend/plugin/{plugin_name}/api/router.py
  2. Read Router List: Extracts app.router list from plugin configuration
  3. Validate List: Ensures router list exists and is properly formatted
  4. Get Router Objects: Uses getattr() to retrieve each named router from the module
  5. Validate Routers: Confirms each object is an instance of APIRouter
  6. Include Router: Adds to main application router with PluginStatusChecker dependency

Example from OAuth2 Plugin:

Configuration in plugin.toml:

Implementation in backend/plugin/oauth2/api/router.py:

Result: All routes in router_v1 become available under /oauth2/v1/* in the main application.

Sources: backend/plugin/tools.py236-262

Router Build Orchestration

The build_final_router() function coordinates the entire injection process:

Build Process Sequence

Critical Ordering: The main router import must occur after extend-level injection but before app-level injection. This ensures:

  1. Extend-level plugins modify app routers before they're assembled into main router
  2. App-level plugins add new top-level routes to the already-assembled main router

Sources: backend/plugin/tools.py265-278


Runtime Status Checking

The PluginStatusChecker class provides per-request plugin status verification, enabling plugins to be enabled or disabled without restarting the application.

Status Checker Architecture

Implementation Details

Class Definition:

The PluginStatusChecker is a callable dependency injected into all plugin routes:

Injection Examples:

Extend-level:

App-level:

Status Storage Format in Redis:

Key: {PLUGIN_REDIS_PREFIX}:{plugin_name} (e.g., fba:plugin:oauth2)

Value (JSON):

Enable/Disable Without Restart

The status checking mechanism allows runtime plugin control:

  1. Admin calls PUT /admin/v1/sys/plugins/{plugin}/status
  2. Service toggles status in Redis: "1""0"
  3. Next request to plugin route checks updated status in Redis
  4. If disabled: Returns 500 error immediately without executing handler
  5. If enabled: Proceeds to route handler execution

No application restart required because status is checked per-request from Redis cache.

Sources: backend/plugin/tools.py430-456 backend/plugin/tools.py226-230 backend/plugin/tools.py260 backend/app/admin/service/plugin_service.py81-100


Dependency Management

Plugins can specify Python package dependencies in requirements.txt, which are automatically installed during plugin installation.

Dependency Installation Flow

Installation Process

Step-by-Step:

  1. Check Requirements File: Looks for backend/plugin/{plugin}/requirements.txt
  2. Parse Dependencies: Reads each line, skipping comments and empty lines
  3. Validate Format: Uses packaging.requirements.Requirement() to parse each dependency
  4. Check Installation: Queries importlib.metadata.distribution() to verify if package is already installed
  5. Mark Missing: Flags any packages that are not found
  6. Ensure pip Available: Runs _ensure_pip_available() which attempts:
    • Check if pip is available: python -m pip --version
    • Install via ensurepip: python -m ensurepip --default-pip
    • Download and install from https://bootstrap.pypa.io/get-pip.py
  7. Build Command: Constructs pip command with optional China mirror index:
    python -m pip install -r requirements.txt [-i {PLUGIN_PIP_INDEX_URL}]
    
  8. Retry Logic: Attempts installation up to PLUGIN_PIP_MAX_RETRY times (default: 3)
  9. Execute: Runs subprocess with stdout/stderr redirected to DEVNULL

Configuration Options:

SettingTypeDescription
PLUGIN_PIP_CHINAboolUse China mirror for faster downloads
PLUGIN_PIP_INDEX_URLstrPyPI mirror URL (default: Aliyun mirror)
PLUGIN_PIP_MAX_RETRYintMaximum retry attempts on failure (default: 3)

Uninstallation Process

The uninstall_requirements() function removes plugin dependencies:

Important Notes:

  • Uninstallation uses -y flag for non-interactive execution
  • Does not check if dependencies are used by other plugins (potential shared dependency issue)
  • Called automatically during plugin uninstallation

Async Wrappers

Both installation and uninstallation have async wrappers for use in async contexts:

  • install_requirements_async() - Uses run_in_threadpool() to avoid blocking event loop
  • uninstall_requirements_async() - Similarly wrapped for async execution

Windows Limitation: Due to Windows asyncio subprocess limitations, these use thread pool execution rather than true async subprocess handling.

Sources: backend/plugin/tools.py281-334 backend/plugin/tools.py336-392 backend/plugin/tools.py394-407 backend/plugin/tools.py410-427


Plugin Lifecycle Management

The system provides comprehensive APIs for managing plugin lifecycle operations including installation, uninstallation, status updates, and packaging.

Installation Sources

Plugins can be installed from two sources:

Installation Architecture

ZIP Installation

The install_zip_plugin() function handles ZIP archive installation:

Validation Steps:

  1. Format Check: Verifies file is valid ZIP using zipfile.is_zipfile()
  2. Structure Check: Ensures archive contains:
    • At least 3 files
    • {plugin_dir}/plugin.toml
    • {plugin_dir}/README.md
  3. Name Extraction: Parses plugin name from filename using regex ^([a-zA-Z0-9_]+)
  4. Conflict Check: Verifies plugin directory doesn't already exist

Extraction Process:

  • Creates plugin directory under PLUGIN_DIR
  • Extracts all files, stripping parent directory from paths
  • Example: plugin_name/api/router.pyapi/router.py

Git Installation

The install_git_plugin() function handles Git repository installation:

Process:

  1. URL Validation: Uses is_git_url() regex to validate format
  2. Repository Name Extraction: Captures repo name from URL
  3. Conflict Check: Verifies plugin doesn't exist
  4. Clone: Uses dulwich.porcelain.clone() with checkout=True

Supported URL Formats:

  • HTTPS: https://github.com/user/repo.git
  • SSH: git@github.com:user/repo.git

Post-Installation

Both installation methods complete with:

  1. Dependency Installation: Calls install_requirements_async(plugin_name)
  2. Change Flag: Sets Redis key {PLUGIN_REDIS_PREFIX}:changed to "ture" (sic)
  3. Return Message: Instructs admin to configure and restart service

Sources: backend/utils/file_ops.py84-138 backend/utils/file_ops.py140-163 backend/app/admin/service/plugin_service.py42-59

Uninstallation

The uninstallation process provides safe plugin removal with backup:

Uninstallation Flow

Process Details:

  1. Environment Check: Only allowed in ENVIRONMENT == 'dev' for safety
  2. Existence Check: Verifies plugin directory exists
  3. Dependency Removal: Calls uninstall_requirements_async(plugin)
  4. Backup Creation: Moves plugin directory to timestamped backup:
    • Original: backend/plugin/{plugin}/
    • Backup: backend/plugin/{plugin}.{YYYYMMDDHHMMSS}.backup/
  5. Cache Cleanup: Deletes plugin info from Redis
  6. Change Flag: Sets changed flag for restart detection
  7. Manual Cleanup: Admin must manually remove configuration and restart

Safety Features:

  • Plugin directory is moved, not deleted - can be restored by renaming
  • Timestamp ensures unique backup names
  • Requires explicit restart to take effect

Sources: backend/app/admin/service/plugin_service.py62-78

Status Management

Plugin status can be toggled between enabled and disabled states without restarting the application:

Status Update Flow

Implementation:

Effect:

  • Next request to any plugin route will check the updated status
  • No restart required - change takes effect immediately
  • All instances see the change (Redis-backed)

Packaging

The system can package installed plugins as ZIP archives for distribution:

Packaging Process:

  1. Validate Existence: Check plugin directory exists
  2. Create BytesIO: In-memory ZIP buffer
  3. Walk Directory: Recursively collect all files, excluding __pycache__
  4. Build Archive: Add files with relative paths, preserving directory structure
  5. Stream Response: Return as StreamingResponse with appropriate headers

API Endpoint:

GET /admin/v1/sys/plugins/{plugin}
Response: application/x-zip-compressed
Filename: {plugin}.zip

Sources: backend/app/admin/service/plugin_service.py103-124 backend/app/admin/api/v1/sys/plugin.py82-89

Admin API Summary

Complete plugin management API:

MethodEndpointPermissionPurpose
GET/admin/v1/sys/pluginsJWT AuthList all plugins with metadata
GET/admin/v1/sys/plugins/changedJWT AuthCheck if restart needed
POST/admin/v1/sys/pluginssys:plugin:installInstall from ZIP or Git
DELETE/admin/v1/sys/plugins/{plugin}sys:plugin:uninstallUninstall (backup + cleanup)
PUT/admin/v1/sys/plugins/{plugin}/statussys:plugin:editToggle enable/disable
GET/admin/v1/sys/plugins/{plugin}JWT AuthDownload as ZIP

Sources: backend/app/admin/api/v1/sys/plugin.py1-89


Redis Caching Strategy

Plugin information is cached in Redis for fast access and cross-instance coordination:

Cache Key Pattern

Primary Keys:

  • {PLUGIN_REDIS_PREFIX}:{plugin_name} - Individual plugin information
  • {PLUGIN_REDIS_PREFIX}:changed - Change detection flag

Default PLUGIN_REDIS_PREFIX is "fba:plugin".

Cache Initialization

During application startup, parse_plugin_config() performs cache synchronization:

Cache Sync Process:

Key Behaviors:

  1. Cleanup Unknown Plugins: Deletes Redis keys for plugins no longer in filesystem
    • Uses delete_prefix() with exclusion list
    • Prevents stale cache entries
  2. Status Preservation: If plugin already cached, preserves existing enable/disable status
  3. Default Enabled: New plugins default to enable = "1"
  4. Full Sync: Overwrites all plugin metadata with latest from plugin.toml
  5. Change Flag Reset: Clears changed flag after successful sync

Cache Updates

Modification Operations:

OperationCache Update
InstallSET changed = "ture" (triggers restart detection)
UninstallDELETE {plugin} + SET changed = "ture"
Status UpdateSET {plugin} (modified enable field only)
Startup SyncSET {plugin} (all plugins) + DELETE changed

Change Detection

The changed key enables restart detection:

Detection Flow:

Use Case: After installing, uninstalling, or modifying plugin files, the system sets this flag. Frontend polls to detect when restart is required to apply changes.

Sources: backend/plugin/tools.py115-173 backend/app/admin/service/plugin_service.py27-39


SQL Script Execution

Plugins can include database initialization scripts for creating tables, indexes, and seeding data:

Script Location

SQL scripts must follow this directory structure:

backend/plugin/{plugin_name}/sql/
├── mysql/
│   ├── init.sql                # For autoincrement primary keys
│   └── init_snowflake.sql      # For Snowflake ID primary keys
└── postgresql/
    ├── init.sql                # For autoincrement primary keys
    └── init_snowflake.sql      # For Snowflake ID primary keys

Script Selection

The get_plugin_sql() function selects the appropriate script based on configuration:

Selection Logic:

Parameters:

  • db_type: DataBaseType.mysql or DataBaseType.postgresql
  • pk_type: PrimaryKeyType.autoincrement or PrimaryKeyType.snowflake

Script Parsing and Validation

The parse_sql_script() utility function validates and parses SQL scripts:

Validation Rules:

  1. File Existence: Script file must exist
  2. Statement Parsing: Uses sqlparse.split() to separate statements
  3. Operation Whitelist: Only SELECT and INSERT statements allowed
    • Prevents destructive operations during parsing
    • Full DDL execution handled separately via database connection

Safety Note: This validation is primarily for the CLI fba --sql command. Plugin installation executes scripts directly via database connection without parsing restrictions.

Sources: backend/plugin/tools.py71-97 backend/utils/file_ops.py166-187


Relationship to Other Systems

The plugin system integrates with multiple core systems:

Application Bootstrap

Plugins are discovered and injected during application startup:

  • build_final_router() called in application factory
  • Must complete before FastAPI app instance is created
  • Affects routing tree structure permanently until restart

For details on application startup sequence, see Application Bootstrap & Lifecycle.

Authentication & Authorization

All plugin routes automatically inherit security dependencies:

  • Extend-level plugins inherit target router's dependencies
  • App-level plugins can define their own dependencies in plugin.toml
  • PluginStatusChecker added as additional dependency

For security implementation details, see RBAC & Permission System.

Database Layer

Plugins can define their own models:

  • get_plugin_models() discovers all plugin model classes
  • Models included in Alembic migration auto-discovery
  • SQL scripts executed during installation for initial schema/data

For database integration details, see Database Schema & Initialization.

CLI Tool

Plugin management exposed via CLI:

  • fba add command installs plugins from ZIP or Git
  • fba --sql command executes plugin SQL scripts
  • See Plugin Management for CLI documentation

Admin Interface

Web UI for plugin management:

  • List installed plugins with metadata
  • Enable/disable plugins in real-time
  • Install new plugins via file upload or Git URL
  • Download plugins as ZIP archives

All operations require appropriate RBAC permissions (sys:plugin:*).


Key Components Reference

Core Functions:

FunctionFilePurpose
get_plugins()backend/plugin/tools.py40-55Discover plugin packages in PLUGIN_DIR
load_plugin_config()backend/plugin/tools.py100-112Load and parse plugin.toml
parse_plugin_config()backend/plugin/tools.py115-173Validate configs, classify plugins, sync to Redis
inject_extend_router()backend/plugin/tools.py176-233Inject extend-level plugin routes
inject_app_router()backend/plugin/tools.py236-262Inject app-level plugin routes
build_final_router()backend/plugin/tools.py265-278Orchestrate complete injection process
install_requirements()backend/plugin/tools.py336-392Install plugin dependencies via pip
uninstall_requirements()backend/plugin/tools.py394-407Remove plugin dependencies

Core Classes:

ClassFilePurpose
PluginStatusCheckerbackend/plugin/tools.py430-456FastAPI dependency for runtime status checking
PluginServicebackend/app/admin/service/plugin_service.py23-127Service layer for plugin lifecycle operations

Exception Classes:

ExceptionRaised When
PluginConfigErrorInvalid or incomplete plugin.toml configuration
PluginInjectErrorRouter injection fails or plugin status check fails
PluginInstallErrorDependency installation or uninstallation fails

File Operations:

FunctionFilePurpose
install_zip_plugin()backend/utils/file_ops.py84-138Install plugin from ZIP archive
install_git_plugin()backend/utils/file_ops.py140-163Install plugin from Git repository
get_plugin_sql()backend/plugin/tools.py71-97Locate SQL initialization script
parse_sql_script()backend/utils/file_ops.py166-187Parse and validate SQL script file

API Endpoints:

EndpointFileDescription
GET /admin/v1/sys/pluginsbackend/app/admin/api/v1/sys/plugin.py18-21List all plugins
GET /admin/v1/sys/plugins/changedbackend/app/admin/api/v1/sys/plugin.py24-27Check restart required
POST /admin/v1/sys/pluginsbackend/app/admin/api/v1/sys/plugin.py30-50Install plugin
DELETE /admin/v1/sys/plugins/{plugin}backend/app/admin/api/v1/sys/plugin.py53-66Uninstall plugin
PUT /admin/v1/sys/plugins/{plugin}/statusbackend/app/admin/api/v1/sys/plugin.py69-79Toggle status
GET /admin/v1/sys/plugins/{plugin}backend/app/admin/api/v1/sys/plugin.py82-89Download plugin