Manifest Reference
Complete manifest.toml schema — plugin info, permissions, hooks, routes, cron, dependencies.
Every plugin has a manifest.toml that defines its identity, permissions, hooks, routes, and cron jobs.
[plugin] — Plugin Identity
[plugin]
id = "com.example.my-plugin"
name = "My Plugin"
version = "0.1.0"
description = "Does something useful"
author = "Alice"
license = "MIT"
runtime = "js"
entry = "main.js"| Field | Required | Default | Description |
|---|---|---|---|
id | yes | — | Unique plugin ID (reverse-domain format recommended) |
name | yes | — | Display name |
version | yes | — | Semantic version |
description | no | "" | Description |
author | no | — | Author name |
license | no | — | License identifier |
runtime | no | "wasm" | Runtime: js / lua / rhai / wasm |
language | no | "rust" | Language identifier |
entry | no | "index.js" | Entry file name |
wasm | no | "plugin.wasm" | WASM file path (runtime = wasm only) |
sdk_version | no | "v1" | SDK version |
Auto-detection: if runtime = "lua" and entry is not set, the loader looks for init.lua. If runtime = "rhai", it looks for init.rhai.
[permissions] — Security
Access is denied unless explicitly declared.
[permissions]
max_memory_mb = 16
timeout_ms = 5000
database = ["read:products", "write:orders", "categories"]
http = ["api.example.com", "*.github.com"]
config = ["app.*", "jwt.*"]
filesystem = ["read-write"]| Field | Default | Description |
|---|---|---|
max_memory_mb | 32 (config) | Memory limit per instance |
timeout_ms | config default | Hook execution timeout |
database | [] (denied) | Database table access |
http | [] (denied) | HTTP whitelist |
config | [] (denied) | Config key whitelist |
filesystem | [] (denied) | VFS access level |
Database Permissions
| Format | Access |
|---|---|
"read:TABLE" | Read-only |
"write:TABLE" | Write-only |
"TABLE" | Full read + write |
"*" | All tables (except protected) |
Protected tables are always blocked regardless of permissions: users, roles, permissions, audit_log, plugin_storage, options, rbac_roles, rbac_permissions, rbac_role_permissions, tenants.
SQL safety: DDL is blocked (CREATE/DROP/ALTER/TRUNCATE). UNION, JOIN, and subqueries are detected and blocked in permission checks.
HTTP Whitelist
- Exact domain:
api.example.com - Wildcard subdomain:
*.github.com - Path wildcard:
api.example.com/v1/*
Built-in SSRF protection: blocks localhost, 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, ::1.
Filesystem Permissions
| Value | Enables |
|---|---|
"read" | vfsRead, vfsExists, vfsList, vfsStat |
"write" | vfsWrite, vfsDelete |
"read-write" | All VFS operations |
"*" | All VFS operations |
[hooks.<name>] — Hook Registration
[hooks.on-content-created]
priority = 50
[hooks.on-content-updating]
priority = 100
match = "product"
[hooks.on-content-updating]
priority = 100
content_types = ["product", "course"]
[hooks.render-markdown]
priority = 10| Field | Default | Description |
|---|---|---|
priority | 100 | Execution order (lower = first) |
match | — | Match by content type name |
content_types | [] (all) | Only fire for specified content types |
Hook names use kebab-case in TOML (on-content-created), auto-converted to snake_case internally (on_content_created).
[[routes]] — Custom API Endpoints
[[routes]]
method = "GET"
path = "/api/v1/plugins/my-plugin/stats"
handler = "getStats"
auth = "public"
[[routes]]
method = "POST"
path = "/api/v1/plugins/my-plugin/contacts/:contactId"
handler = "updateContact"
auth = "admin"
description = "Update a contact"| Field | Required | Default | Description |
|---|---|---|---|
method | yes | — | HTTP method |
path | yes | — | Route path, supports :param placeholders |
handler | yes | — | Function name in the plugin |
auth | no | default | public / member / admin |
description | no | — | Description |
permission | no | — | Extra permission requirement |
Handlers receive input with { path, method, body, headers, params }. Return data directly — the framework wraps it in { code: 0, data: ... }.
Route Input Parameters
[[routes]]
method = "POST"
path = "/api/v1/plugins/my-plugin/deals"
handler = "createDeal"
[[routes.input]]
name = "title"
type = "string"
in = "body"
required = true
description = "Deal title"
[[routes.input]]
name = "page"
type = "integer"
in = "query"
default = 1
description = "Page number"| Field | Required | Default | Description |
|---|---|---|---|
name | yes | — | Parameter name |
in | no | "query" | Location: query / body / path / header |
type | no | "" | Type hint |
required | no | false | Whether required |
description | no | — | Description |
default | no | — | Default value |
Route Output
[routes.output]
description = "Deal list"
[[routes.output.fields]]
name = "id"
type = "string"
description = "Deal ID"
[[routes.output.fields]]
name = "title"
type = "string"
description = "Deal title"[[cron]] — Scheduled Tasks
[[cron]]
label = "Daily Cleanup"
job_type = "daily_cleanup"
cron_expr = "0 0 * * *"
payload = """{"type": "full"}"""
enabled = true| Field | Required | Default | Description |
|---|---|---|---|
label | yes | — | Task name |
job_type | yes | — | Task type (passed to on_cron_tick) |
cron_expr | yes | — | Cron expression (seven segments including seconds) |
payload | no | — | JSON payload string |
enabled | no | true | Whether enabled |
Cron entries are synced to the database on plugin load and removed on unload.
[[content_types]] — Bundle Content Types
[[content_types]]
file = "content_types/contact.toml"Plugins can ship their own Content Type TOML files. They are auto-loaded when the plugin is installed.
[[admin_pages]] — Admin UI Pages
[[admin_pages]]
path = "/admin/plugins/crm"
label = "CRM"
icon = "users"
component = "CrmDashboard"| Field | Required | Description |
|---|---|---|
path | yes | Page path |
label | yes | Display name |
icon | no | Icon name |
component | no | Frontend component name |
[dependencies] — Plugin Dependencies
[dependencies]
"com.example.base" = "1.0.0"Plugins are loaded in topological sort order to ensure dependencies initialize first.
Full Example
[plugin]
id = "com.example.crm"
name = "CRM API"
version = "0.1.0"
description = "Sales pipeline and contact management"
author = "Alice"
license = "MIT"
runtime = "js"
entry = "main.js"
[permissions]
max_memory_mb = 16
timeout_ms = 5000
database = ["crm_contacts", "crm_companies", "crm_deals"]
http = ["api.example.com"]
config = ["app.*"]
filesystem = ["read-write"]
[hooks.on-content-created]
priority = 50
[hooks.on-content-updating]
priority = 100
content_types = ["contact"]
[hooks.render-markdown]
priority = 10
[[routes]]
method = "GET"
path = "/api/v1/plugins/crm/pipeline"
handler = "getPipeline"
auth = "public"
[[routes]]
method = "GET"
path = "/api/v1/plugins/crm/pipeline/:dealId"
handler = "getDealDetail"
auth = "member"
[[cron]]
label = "Daily Stats"
job_type = "daily_stats"
cron_expr = "0 0 * * *"
payload = """{"type": "full"}"""
[dependencies]
"com.example.base" = "1.0.0"