DeployStack Plugin System
This document explains how to create and integrate plugins into DeployStack. The plugin system enables extending DeployStack with additional functionality, cloud providers, database tables, APIs, and UI components.Overview
DeployStack’s plugin architecture allows for extensible, modular development with built-in security and isolation. Plugins can:- Add new database tables and schemas
- Register new API routes (automatically namespaced for security)
- Extend core functionality
- Add support for additional cloud providers
- Implement custom business logic
- Define global settings and configuration groups
Security Features
Route Isolation & Security
DeployStack implements strict route isolation to ensure plugins cannot interfere with core functionality or each other:- Automatic Namespacing: All plugin routes are automatically prefixed with
/api/plugin/<plugin-id>/
- No Direct Route Access: Plugins cannot register routes directly on the global Fastify instance
- Sandboxed Registration: Plugins use
PluginRouteManager
which enforces namespacing - Core Route Protection: Plugins cannot access or modify core routes (
/api/auth/*
,/api/users/*
, etc.)
Security Benefits
- Prevents Route Hijacking: Malicious plugins cannot override authentication or user management routes
- Eliminates Route Conflicts: Multiple plugins cannot register conflicting routes
- Predictable API Surface: All plugin APIs follow consistent
/api/plugin/<name>/
structure - Easy Auditing: Route ownership is immediately clear from the URL structure
- Fail-Safe Design: Plugins that don’t follow the new system simply won’t have routes registered
Example Security Enforcement
Plugin Structure
A basic plugin consists of the following files:Required Files
- package.json - Defines plugin metadata and dependencies
- index.ts - Implements the Plugin interface and exports the plugin class
- routes.ts - Contains all API route definitions (automatically namespaced)
- schema.ts - (Optional) Contains database schema extensions
File Responsibilities
- index.ts: Plugin metadata, database extensions, global settings, non-route initialization
- routes.ts: All API route definitions using the isolated
PluginRouteManager
- schema.ts: Database table definitions and schema extensions
- package.json: Plugin metadata and dependency declarations
Creating a New Plugin
1. Create Plugin Directory
Create a directory for your plugin:2. Create package.json
Add basic plugin information:3. Define Database Schema (Optional)
If your plugin requires database tables, create aschema.ts
file:
4. Create Routes File
Create aroutes.ts
file for your API routes:
5. Implement the Plugin Interface
Create anindex.ts
file that implements the Plugin interface:
Plugin Integration Points
Database Extension
ThedatabaseExtension
property allows your plugin to:
- Define tables dynamically: Tables are created at runtime from your
tableDefinitions
- Initialize data: Seed data or perform setup through
onDatabaseInit
- Maintain security boundaries: Plugin tables are isolated from core migrations
How Plugin Database Tables Work
Security Architecture:- Phase 1 (Trusted): Core migrations run first (static, secure)
- Phase 2 (Untrusted): Plugin tables created dynamically (sandboxed)
- Clear Separation: Plugin tables cannot interfere with core database structure
- Plugin tables are NOT included in core migration files
- Tables are created at runtime from your
tableDefinitions
- System automatically generates CREATE TABLE SQL from your definitions
- Tables are dropped and recreated during development for clean structure
- Use
created_at
(snake_case) for database column names, notcreatedAt
(camelCase) - Timestamp columns with
{ mode: 'timestamp' }
automatically getDEFAULT (strftime('%s', 'now'))
- Column types are auto-detected:
id
/count
→ INTEGER,*_at
/*date
→ INTEGER (timestamp), others → TEXT - Tables are prefixed with your plugin ID:
my-plugin_my_entities
API Routes
Register API routes using the isolatedPluginRouteManager
:
Event Listeners
Plugins can listen to system events and react to core application changes:Access to Core Services
Plugins receive access to:- Database instance (
db
) - For database operations with your plugin tables - Route Manager (
routeManager
) - For registering isolated, namespaced routes - Logger (
logger
) - For structured logging with plugin context - Schema Access - Access to the generated database schema including your tables
- Global Settings - Plugins can define and access their own global settings
- Event System - Listen to and react to core application events
Plugin Lifecycle
Plugins follow this lifecycle:- Discovery - Plugin is discovered and loaded from the plugins directory
- Registration - Plugin table definitions are registered with the schema system
- Core Database Setup - Core migrations are applied (trusted, static)
- Plugin Table Creation - Plugin tables are created dynamically from definitions
- Database Initialization -
onDatabaseInit
is called for data seeding/setup - Plugin Initialization -
initialize
method is called for non-route setup - Route Registration -
registerRoutes
is called to register API endpoints - Runtime - Plugin operates as part of the application
- Shutdown -
shutdown
method is called during application termination
Database Lifecycle Details
The database initialization follows a strict security-first approach:Testing Your Plugin
To test your plugin:- Place it in the
plugins
directory - Start the DeployStack server
- Check server logs for initialization messages
- Test your plugin’s API endpoints
Advanced Plugin Features
Configuration
Your plugin can access configuration provided by the plugin manager:Plugin Manager APIs
Plugins can access other plugins through the plugin manager:Frontend Integration
If your plugin needs to extend the UI, you can:- Register API endpoints that provide UI configuration
- Use the Plugin Manager to register UI components
- Follow frontend plugin documentation for UI extensions
Best Practices
- Unique IDs - Ensure your plugin ID is unique and descriptive
- Error Handling - Properly handle errors in your plugin
- Database Relationships - Be careful with cross-plugin table relationships
- Schema Design - Follow naming conventions for your plugin’s tables
- Documentation - Include a README.md with your plugin
- Versioning - Use semantic versioning for your plugin
Troubleshooting
Plugin Not Loading
- Check plugin directory structure
- Ensure your plugin class is exported as default
- Verify package.json contains required fields
Database Errors
- Check your schema definitions
- Ensure proper initialization in
onDatabaseInit
- Verify SQL queries in your plugin
Integration Issues
- Look for errors during plugin initialization
- Check console logs for error messages
- Verify API routes are registered correctly
Example Plugins
See theplugins/example-plugin
directory for a working example.
Plugin API Reference
The complete Plugin interface is defined insrc/plugin-system/types.ts
.
Defining Global Settings via Plugins
Plugins can contribute their own global settings to the DeployStack system. These settings will be managed alongside core global settings and will be editable by users with theglobal_admin
role.
How it Works
- Define
globalSettingsExtension
: In your plugin class, add an optional propertyglobalSettingsExtension
. - Structure: This property should be an object implementing the
GlobalSettingsExtension
interface (defined insrc/plugin-system/types.ts
). It can contain:
groups
: An optional array ofGlobalSettingGroupForPlugin
objects to define new setting groups.settings
: A mandatory array ofGlobalSettingDefinitionForPlugin
objects to define individual settings.
- Initialization: During server startup, the
PluginManager
will:
- Collect all group and setting definitions from active plugins.
- Create any new groups defined by plugins if they don’t already exist. If a group ID already exists, the plugin’s group definition is ignored for that specific group, and the existing group is used.
- Initialize the plugin’s global settings with their default values, but only if a setting with the same key doesn’t already exist (either from core settings or another plugin). Core settings always take precedence.
-
Access Control: All plugin-defined global settings are subject to the same access control as core settings (i.e., manageable by
global_admin
). - Security:
- Core Precedence: Core global settings (defined in
services/backend/src/global-settings/
) cannot be overridden by plugins. - Duplicate Keys: If a plugin attempts to register a setting with a key that already exists (from core or another plugin), the plugin’s setting will be ignored, and a warning will be logged.
Example: Defining Global Settings in a Plugin
Important Considerations
- Key Uniqueness: Ensure your setting keys are unique, preferably prefixed with your plugin ID (e.g.,
yourPluginId.category.settingName
) to avoid conflicts. - Group IDs: If defining new groups, ensure their IDs are unique.
- Default Values: Provide sensible default values.
- Encryption: Mark sensitive settings (API keys, passwords) with
encrypted: true
. - Documentation: Document any global settings your plugin introduces in its own README or documentation.
For additional questions or support, please contact the DeployStack team or open an issue on GitHub.