Version: 1.11.0 | Updated: 2025-02-08
- Plugins extend FluxStack with custom functionality
- Located in
plugins/[plugin-name]/directory - Use lifecycle hooks for integration
- Support declarative configuration system
- Can add CLI commands, routes, and middleware
- Auto-discovered and loaded at startup
plugins/my-plugin/
├── index.ts # Main plugin file (required)
├── package.json # Plugin metadata
├── config/
│ └── index.ts # Plugin configuration
├── server/
│ ├── index.ts # Server-side code
│ └── middleware.ts # Middleware
├── client/
│ └── index.ts # Client-side code
└── cli/
└── commands.ts # CLI commands
// plugins/my-plugin/index.ts
import type { FluxStack, PluginContext } from "@core/plugins/types"
import { Elysia } from "elysia"
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
version: "1.0.0",
description: "My custom plugin",
author: "Your Name",
priority: 100,
category: "utility",
tags: ["custom", "utility"],
dependencies: [],
setup: async (context: PluginContext) => {
context.logger.info('My plugin initialized')
// Initialize plugin services
// Register middleware
// Setup database connections
},
onServerStart: async (context: PluginContext) => {
context.logger.info('Server started with my plugin')
},
onRequest: async (requestContext) => {
// Process incoming requests
},
onResponse: async (responseContext) => {
// Process outgoing responses
}
}
export default myPlugininterface Plugin {
// Metadata
name: string
version?: string
description?: string
author?: string
dependencies?: string[]
priority?: number | 'highest' | 'high' | 'normal' | 'low' | 'lowest'
category?: string
tags?: string[]
// Lifecycle hooks
setup?: (context: PluginContext) => void | Promise<void>
onConfigLoad?: (context: ConfigLoadContext) => void | Promise<void>
onBeforeServerStart?: (context: PluginContext) => void | Promise<void>
onServerStart?: (context: PluginContext) => void | Promise<void>
onAfterServerStart?: (context: PluginContext) => void | Promise<void>
onBeforeServerStop?: (context: PluginContext) => void | Promise<void>
onServerStop?: (context: PluginContext) => void | Promise<void>
// Request/Response hooks
onRequest?: (context: RequestContext) => void | Promise<void>
onBeforeRoute?: (context: RequestContext) => void | Promise<void>
onAfterRoute?: (context: RouteContext) => void | Promise<void>
onBeforeResponse?: (context: ResponseContext) => void | Promise<void>
onResponse?: (context: ResponseContext) => void | Promise<void>
onRequestValidation?: (context: ValidationContext) => void | Promise<void>
onResponseTransform?: (context: TransformContext) => void | Promise<void>
// Error handling
onError?: (context: ErrorContext) => void | Promise<void>
// Build hooks
onBeforeBuild?: (context: BuildContext) => void | Promise<void>
onBuild?: (context: BuildContext) => void | Promise<void>
onBuildComplete?: (context: BuildContext) => void | Promise<void>
onBuildError?: (context: BuildErrorContext) => void | Promise<void>
// CLI commands
commands?: CliCommand[]
}Called during plugin initialization, before server starts:
setup: async (context: PluginContext) => {
// Initialize services
const service = new MyService(context.config)
// Store in global for access in other hooks
;(global as any).myService = service
// Register with plugin registry
context.logger.info('Plugin initialized')
}Called when server starts:
onServerStart: async (context: PluginContext) => {
context.logger.info('Server started')
// Start background tasks
// Connect to external services
// Initialize monitoring
}Process incoming requests:
onRequest: async (requestContext) => {
// Log request
console.log(`${requestContext.method} ${requestContext.path}`)
// Add custom headers
requestContext.headers['x-custom'] = 'value'
// Authenticate user
const user = await authenticateRequest(requestContext)
requestContext.user = user
}Process outgoing responses:
onResponse: async (responseContext) => {
// Log response
console.log(`${responseContext.statusCode} - ${responseContext.duration}ms`)
// Track metrics
if (responseContext.user) {
trackUserActivity(responseContext.user, responseContext.path)
}
}Handle errors:
onError: async (errorContext) => {
// Log error
console.error('Request error:', errorContext.error)
// Send to error tracking service
await sendToSentry(errorContext.error)
// Mark as handled to prevent default error handler
errorContext.handled = true
}Use declarative config system:
// plugins/my-plugin/config/index.ts
import { defineConfig, config } from '@core/utils/config-schema'
const myPluginConfigSchema = {
enabled: config.boolean('MY_PLUGIN_ENABLED', true),
apiKey: config.string('MY_PLUGIN_API_KEY', '', true), // required
timeout: config.number('MY_PLUGIN_TIMEOUT', 5000, false),
features: config.array('MY_PLUGIN_FEATURES', ['feature1', 'feature2'])
}
export const myPluginConfig = defineConfig(myPluginConfigSchema)
export type MyPluginConfig = typeof myPluginConfig
export default myPluginConfigUse in plugin:
// plugins/my-plugin/index.ts
import { myPluginConfig } from "./config"
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
setup: async (context) => {
if (!myPluginConfig.enabled) {
context.logger.info('Plugin disabled')
return
}
const service = new MyService({
apiKey: myPluginConfig.apiKey,
timeout: myPluginConfig.timeout
})
}
}Plugins can add routes using Elysia:
import { Elysia, t } from "elysia"
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
// @ts-ignore - plugin property supported but not in official types
plugin: new Elysia({ prefix: "/api/my-plugin", tags: ['MyPlugin'] })
.get("/status", () => ({
status: "ok",
version: "1.0.0"
}), {
response: t.Object({
status: t.String(),
version: t.String()
}),
detail: {
summary: 'Plugin Status',
description: 'Returns plugin status information'
}
})
.post("/action", async ({ body }) => {
// Handle action
return { success: true }
}, {
body: t.Object({
data: t.String()
}),
response: t.Object({
success: t.Boolean()
})
})
}// plugins/my-plugin/cli/my-command.ts
import type { CliCommand } from "@core/plugins/types"
export const myCommand: CliCommand = {
name: "my:command",
description: "Does something useful",
usage: "flux my:command [options]",
examples: [
"flux my:command --option value"
],
options: [
{
name: "option",
alias: "o",
description: "An option",
type: "string",
required: false
}
],
handler: async (args, options, context) => {
context.logger.info('Running my command')
// Access config
const config = context.config
// Perform action
console.log('Option value:', options.option)
}
}Register in plugin:
import { myCommand } from "./cli/my-command"
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
commands: [myCommand]
}Create reusable middleware:
// plugins/my-plugin/server/middleware.ts
export class MyMiddleware {
constructor(private config: any) {}
async handle(requestContext: RequestContext) {
// Validate request
if (!this.validateRequest(requestContext)) {
throw new Error('Invalid request')
}
// Add data to context
requestContext.user = await this.getUser(requestContext)
}
private validateRequest(context: RequestContext): boolean {
// Validation logic
return true
}
private async getUser(context: RequestContext) {
// Get user from headers
return { id: 1, name: 'User' }
}
}Use in plugin:
import { MyMiddleware } from "./server/middleware"
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
setup: async (context) => {
const middleware = new MyMiddleware(myPluginConfig)
;(global as any).myMiddleware = middleware
},
onRequest: async (requestContext) => {
const middleware = (global as any).myMiddleware
await middleware.handle(requestContext)
}
}{
"name": "@fluxstack/my-plugin",
"version": "1.0.0",
"description": "My FluxStack plugin",
"main": "index.ts",
"types": "index.ts",
"exports": {
".": {
"import": "./index.ts",
"types": "./index.ts"
},
"./server": {
"import": "./server/index.ts",
"types": "./server/index.ts"
},
"./client": {
"import": "./client/index.ts",
"types": "./client/index.ts"
}
},
"keywords": [
"fluxstack",
"plugin"
],
"author": "Your Name",
"license": "MIT",
"dependencies": {},
"fluxstack": {
"plugin": true,
"version": "^1.0.0",
"hooks": [
"setup",
"onServerStart",
"onRequest"
],
"category": "utility",
"tags": ["custom"]
}
}Declare dependencies on other plugins:
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
dependencies: ["crypto-auth", "database"],
setup: async (context) => {
// Dependencies are loaded first
// Access other plugin services
const authService = (global as any).cryptoAuthService
}
}Control load order with priority:
export const myPlugin: FluxStack.Plugin = {
name: "my-plugin",
priority: 100, // Higher = loads first
// or use named priorities:
// priority: 'highest' | 'high' | 'normal' | 'low' | 'lowest'
}Load order:
- Highest priority (or 1000+)
- High priority (or 500-999)
- Normal priority (or 100-499) - default
- Low priority (or 50-99)
- Lowest priority (or 0-49)
Only whitelisted plugins are loaded:
// config/system/plugins.config.ts
export const pluginsConfig = defineConfig({
whitelist: config.array('PLUGINS_WHITELIST', [
'crypto-auth',
'my-plugin'
])
})Always validate user input in plugin routes:
.post("/action", async ({ body, set }) => {
if (!body.data || typeof body.data !== 'string') {
set.status = 400
return { error: 'Invalid data' }
}
// Process validated data
}, {
body: t.Object({
data: t.String({ minLength: 1, maxLength: 1000 })
})
})Never expose sensitive config in responses:
// ❌ BAD
.get("/config", () => myPluginConfig)
// ✅ GOOD
.get("/config", () => ({
enabled: myPluginConfig.enabled,
features: myPluginConfig.features
// Don't expose apiKey or secrets
}))// plugins/my-plugin/__tests__/plugin.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import { myPlugin } from '../index'
describe('MyPlugin', () => {
it('should have correct metadata', () => {
expect(myPlugin.name).toBe('my-plugin')
expect(myPlugin.version).toBe('1.0.0')
})
it('should initialize correctly', async () => {
const mockContext = {
config: {},
logger: { info: vi.fn() },
app: {},
utils: {}
}
await myPlugin.setup?.(mockContext)
expect(mockContext.logger.info).toHaveBeenCalled()
})
})Reference implementation in plugins/crypto-auth/:
export const cryptoAuthPlugin: FluxStack.Plugin = {
name: "crypto-auth",
version: "1.0.0",
description: "Ed25519 cryptographic authentication",
priority: 100,
category: "auth",
tags: ["authentication", "ed25519", "security"],
dependencies: [],
setup: async (context) => {
if (!cryptoAuthConfig.enabled) return
const authService = new CryptoAuthService({
maxTimeDrift: cryptoAuthConfig.maxTimeDrift,
adminKeys: cryptoAuthConfig.adminKeys,
logger: context.logger
})
;(global as any).cryptoAuthService = authService
},
plugin: new Elysia({ prefix: "/api/auth" })
.get("/info", () => ({
name: "FluxStack Crypto Auth",
version: "1.0.0"
})),
onResponse: async (context) => {
if (!cryptoAuthConfig.enableMetrics) return
// Log authentication metrics
if (context.user) {
console.debug("Authenticated request", {
publicKey: context.user.publicKey,
path: context.path
})
}
}
}Plugins are auto-discovered from:
plugins/directory (project plugins)node_modules/@fluxstack/*-plugin(npm plugins)- Whitelisted in
config/system/plugins.config.ts
ALWAYS:
- Export plugin as default export
- Use declarative config system
- Validate all user input
- Handle errors gracefully
- Document plugin hooks and dependencies
- Test plugin functionality
NEVER:
- Modify core framework files
- Expose sensitive configuration
- Block server startup in setup hook
- Ignore security best practices
- Forget to cleanup in onServerStop