Plugin Configuration
Plugin Configuration
All plugin settings live in modula.config.json. This page documents every field with detailed explanations of behavior and tuning guidance.
Complete Configuration Block
{
"plugin_enabled": true,
"plugin_directory": "./plugins/",
"plugin_max_vms": 4,
"plugin_timeout": 5,
"plugin_max_ops": 1000,
"plugin_hot_reload": false,
"plugin_max_failures": 5,
"plugin_reset_interval": "60s",
"plugin_rate_limit": 100,
"plugin_max_routes": 50,
"plugin_max_request_body": 1048576,
"plugin_max_response_body": 5242880,
"plugin_trusted_proxies": [],
"plugin_hook_reserve_vms": 1,
"plugin_hook_max_consecutive_aborts": 10,
"plugin_hook_max_ops": 100,
"plugin_hook_max_concurrent_after": 10,
"plugin_hook_timeout_ms": 2000,
"plugin_hook_event_timeout_ms": 5000
}
Master Switch and Directory
plugin_enabled
| Type | Default |
|---|---|
| bool | false |
Master switch for the entire plugin system. When false, no plugins are loaded, no routes are registered, and no hooks fire. The plugin directory is not scanned.
Set to true to enable plugin loading on server startup.
plugin_directory
| Type | Default |
|---|---|
| string | "./plugins/" |
Path to the directory scanned for plugin subdirectories. Can be absolute or relative to the CMS working directory. Each subdirectory containing an init.lua file is treated as a plugin candidate.
VM Pool Tuning
plugin_max_vms
| Type | Default | Range |
|---|---|---|
| int | 4 |
1+ |
Total VM pool size per plugin for HTTP and hook execution. This count is split between two channels:
- General channel:
plugin_max_vms - plugin_hook_reserve_vmsVMs. Serves HTTP request handlers. - Reserved channel:
plugin_hook_reserve_vmsVMs. Serves content hooks exclusively.
The two-channel design guarantees hook execution even when HTTP traffic saturates the general pool. HTTP requests that cannot acquire a VM within 100ms receive a 503 response.
A separate UI VM pool (plugin_max_ui_vms) is used for TUI screens and field interfaces. See below.
Tuning guidance:
- For plugins with light HTTP traffic and few hooks:
2is sufficient. - For plugins under heavy HTTP load: increase to
8-16. Each VM is a gopher-lua state with its own stack and globals, consuming roughly 1-2 MB of memory. - The value must be greater than
plugin_hook_reserve_vms.
plugin_hook_reserve_vms
| Type | Default | Range |
|---|---|---|
| int | 1 |
0+ |
Number of VMs reserved exclusively for hook execution. These VMs are never used for HTTP requests.
Setting this to 0 means hooks compete with HTTP requests for the same VM pool. This is acceptable for plugins that do not register hooks, but risks hook starvation under HTTP load for plugins that do.
Must be less than plugin_max_vms.
plugin_max_ui_vms
| Type | Default | Range |
|---|---|---|
| int | 4 |
1+ |
Maximum concurrent TUI screen and field interface sessions per plugin. Each active plugin screen or field interface in the SSH TUI holds one UI VM for the lifetime of the session.
UI VMs are in a separate pool from the general and reserved pools. They have no acquisition timeout -- if the pool is full, the screen shows "Plugin busy" instead of blocking.
In a multi-user SSH environment, each concurrent operator viewing a plugin screen or editing a plugin field consumes one UI VM. With the default of 4, four operators can use plugin UIs simultaneously per plugin.
Only created for plugins that declare screens or interfaces in their manifest. Plugins without UI declarations do not allocate UI VMs.
Timeout Settings
plugin_timeout
| Type | Default | Unit |
|---|---|---|
| int | 5 |
seconds |
Maximum execution time for a single HTTP request handler or after-hook. If the Lua code does not return within this duration, the VM execution is killed and an error is returned (500 for HTTP, logged for hooks).
This is the outer timeout. Individual database operations within the handler have their own timeouts derived from this value.
plugin_hook_timeout_ms
| Type | Default | Unit |
|---|---|---|
| int | 2000 |
milliseconds |
Maximum execution time for a single before-hook handler. Before-hooks run synchronously inside the CMS database transaction, so this timeout is intentionally short to prevent long-running hooks from blocking CMS operations.
If a before-hook exceeds this timeout, it is treated as an error: the hook's circuit breaker is incremented, and the transaction is aborted.
plugin_hook_event_timeout_ms
| Type | Default | Unit |
|---|---|---|
| int | 5000 |
milliseconds |
Maximum total time for the entire before-hook chain on a single event. Multiple plugins may register before-hooks on the same event and table. This timeout caps the total time spent running all of them.
If the chain exceeds this timeout, remaining hooks are skipped and the transaction is aborted.
This timeout applies to the chain as a whole, not to individual hooks. A chain of 5 hooks that each take 900ms would exceed the 5000ms event timeout, even though each individual hook is under the 2000ms per-hook timeout.
Operation Budgets
plugin_max_ops
| Type | Default |
|---|---|
| int | 1000 |
Maximum number of database operations (db.insert, db.query, db.update, db.delete, etc.) allowed per HTTP request handler execution. Each db.* call decrements the budget. When the budget reaches zero, subsequent db.* calls raise a Lua error.
This prevents runaway queries from monopolizing database connections. A well-designed handler typically uses 5-20 operations.
plugin_hook_max_ops
| Type | Default |
|---|---|
| int | 100 |
Maximum database operations per after-hook execution. After-hooks run asynchronously with fire-and-forget semantics, so the budget is lower than for HTTP handlers.
Before-hooks always have a budget of zero. All db.* calls are blocked inside before-hooks because they run inside the CMS database transaction, and plugin database operations use a separate connection pool. Calling db.* in a before-hook would deadlock on SQLite (single-writer) and risk long-held locks on MySQL/PostgreSQL.
Rate Limiting
plugin_rate_limit
| Type | Default | Unit |
|---|---|---|
| int | 100 |
requests per second |
Per-IP token bucket rate limit for plugin HTTP endpoints. Each unique client IP gets its own bucket.
When a client exceeds the rate, the plugin bridge returns HTTP 429 (Too Many Requests).
Implementation details:
- Token bucket with burst equal to the rate (e.g., 100 tokens, refilling at 100/s).
- Inactive entries (no requests for 10 minutes) are cleaned up every 5 minutes to prevent memory growth.
- The rate applies per-IP across all plugins, not per-plugin.
plugin_trusted_proxies
| Type | Default |
|---|---|
| []string | [] |
CIDR ranges for trusted reverse proxies. When a request arrives from a trusted proxy IP, the X-Forwarded-For header is parsed to extract the real client IP for rate limiting and req.client_ip.
{
"plugin_trusted_proxies": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
}
If empty, req.client_ip uses the direct connection IP.
Size Limits
plugin_max_routes
| Type | Default |
|---|---|
| int | 50 |
Maximum number of HTTP routes a single plugin can register. Attempting to register more routes raises an error during plugin loading.
plugin_max_request_body
| Type | Default | Unit |
|---|---|---|
| int | 1048576 |
bytes (1 MB) |
Maximum request body size for plugin HTTP endpoints. Requests with a body exceeding this size are rejected with HTTP 413 (Request Entity Too Large) before the handler executes.
plugin_max_response_body
| Type | Default | Unit |
|---|---|---|
| int | 5242880 |
bytes (5 MB) |
Maximum response body size for plugin HTTP endpoints. Responses exceeding this size are truncated.
Circuit Breaker Tuning
plugin_max_failures
| Type | Default |
|---|---|
| int | 5 |
Consecutive failures before the plugin-level circuit breaker trips. Failures include HTTP handler errors and manager operation errors (reload, init). Hook failures do not count.
When the circuit breaker is open, all HTTP requests to the plugin return 503 until the reset interval elapses.
plugin_reset_interval
| Type | Default |
|---|---|
| string | "60s" |
Duration before the circuit breaker transitions from open to half-open. Accepts Go duration strings: "30s", "2m", "1m30s".
In the half-open state, one probe request is allowed through. If it succeeds, the breaker closes (normal operation). If it fails, the breaker re-opens for another reset interval.
An admin can force-reset the circuit breaker at any time with modulacms plugin enable <name>.
plugin_hook_max_consecutive_aborts
| Type | Default |
|---|---|
| int | 10 |
Consecutive errors before a hook-level circuit breaker trips. Each (plugin, event, table) combination has its own breaker. When tripped, that specific hook is disabled until the plugin is reloaded or re-enabled.
Hook circuit breakers are independent of the plugin-level circuit breaker.
plugin_hook_max_concurrent_after
| Type | Default |
|---|---|
| int | 10 |
Maximum number of after-hook goroutines running concurrently across all plugins. After-hooks are fire-and-forget, but this semaphore prevents unbounded goroutine growth during burst writes.
When the limit is reached, additional after-hooks block until a slot becomes available or the shutdown context is cancelled.
Hot Reload Settings
plugin_hot_reload
| Type | Default |
|---|---|
| bool | false |
Enable file-polling watcher for live plugin reload during development. When true, the system polls for .lua file changes every 2 seconds.
Change detection uses SHA-256 checksums of all .lua files in the plugin directory. When a checksum changes, the plugin undergoes blue-green reload: a new instance is created alongside the old one. If the new instance loads successfully, it replaces the old one atomically. If the new instance fails, the old one keeps running.
Safety limits (not configurable):
| Limit | Value | Purpose |
|---|---|---|
| Debounce delay | 1 second | Wait for file writes to settle before reloading |
| Reload cooldown | 10 seconds per plugin | Prevent reload storms during rapid iteration |
| Max .lua files per plugin | 100 | Prevent DoS via file count during checksumming |
| Max total size per checksum | 10 MB | Prevent DoS via file size during checksumming |
| Max consecutive slow reloads | 3 (>10s each) | Pause watcher for plugin on systemic issues |
Do not enable in production. File polling adds overhead, and the debounce/cooldown windows create brief periods where changes are not yet live. Use modulacms plugin reload <name> for controlled production updates.
Example Configurations
Development
Generous limits, hot reload enabled, fast circuit breaker reset for iterative development:
{
"plugin_enabled": true,
"plugin_directory": "./plugins/",
"plugin_max_vms": 2,
"plugin_timeout": 10,
"plugin_max_ops": 5000,
"plugin_hot_reload": true,
"plugin_max_failures": 10,
"plugin_reset_interval": "10s",
"plugin_rate_limit": 1000,
"plugin_max_routes": 100,
"plugin_max_request_body": 10485760,
"plugin_max_response_body": 10485760,
"plugin_trusted_proxies": [],
"plugin_hook_reserve_vms": 1,
"plugin_hook_max_consecutive_aborts": 50,
"plugin_hook_max_ops": 500,
"plugin_hook_max_concurrent_after": 20,
"plugin_hook_timeout_ms": 5000,
"plugin_hook_event_timeout_ms": 10000
}
Production
Strict limits, hot reload off, trusted proxy for load balancer:
{
"plugin_enabled": true,
"plugin_directory": "/opt/modulacms/plugins/",
"plugin_max_vms": 8,
"plugin_timeout": 5,
"plugin_max_ops": 500,
"plugin_hot_reload": false,
"plugin_max_failures": 3,
"plugin_reset_interval": "120s",
"plugin_rate_limit": 50,
"plugin_max_routes": 30,
"plugin_max_request_body": 524288,
"plugin_max_response_body": 2097152,
"plugin_trusted_proxies": ["10.0.0.0/8"],
"plugin_hook_reserve_vms": 2,
"plugin_hook_max_consecutive_aborts": 5,
"plugin_hook_max_ops": 50,
"plugin_hook_max_concurrent_after": 5,
"plugin_hook_timeout_ms": 1000,
"plugin_hook_event_timeout_ms": 3000
}
Key differences from development:
- Higher VM count (8) to handle concurrent load, with 2 reserved for hooks.
- Lower operation budgets to limit blast radius of misbehaving plugins.
- Lower rate limit per IP.
- Smaller request/response body limits.
- Shorter hook timeouts.
- Stricter circuit breaker thresholds (trip after 3 failures, 2-minute reset).
- Hot reload disabled -- use
modulacms plugin reloadfor controlled updates.