manifest.json reference
Legacy v2 reference
This page documents the flat v2 manifest (APIVersion = "2.0.0"). The canonical contract is now Module Contract v3 (apiVersion: asteby.com/v3) — see the Manifest concept and the kernel's v3 spec. The kernel dual-reads v2 for compatibility, so this reference still applies to existing addons and to SDK tooling output until it finishes migrating to v3.
The manifest is the single contract between an addon and the metacore kernel. It is consumed by Go (kernel/manifest) and mirrored by the TS SDK via tygo. This (legacy) document reflects APIVersion = "2.0.0".
When this spec evolves, APIVersion is bumped and a migration path is documented. Addons opt into a compatibility window via the top-level kernel field.
Table of contents
- Top-level fields
- 1. Identity
- 2. Tenant isolation
- 3.
model_definitions[] - 4.
navigation[] - 5.
actions{} - 6.
tools[] - 7.
capabilities[] - 8.
hooks{}andlifecycle_hooks{} - 9.
settings[] - 10.
frontend{} - 11.
backend{} - 12.
signature{} - 13.
events[]
Top-level fields
| Field | Type | Required | Section |
|---|---|---|---|
key, name, version, kernel | string | yes (key/name/version) | Identity |
description, category, author, website, license, icon_* | string | no | Identity |
tenant_isolation | enum | no (default "shared") | Tenant isolation |
model_definitions | array | no | Models |
navigation | array | no | Navigation |
actions | object | no | Actions |
tools | array | no | Tools |
capabilities | array | recommended | Capabilities |
hooks, lifecycle_hooks | object | no | Hooks |
settings | array | no | Settings |
frontend | object | no | Frontend |
backend | object | no | Backend |
signature | object | stamped at publish | Signature |
events | string[] | no | Events |
i18n | object | no | Locale → namespace tree, merged into the host's i18next via I18nProvider. |
1. Identity
{
"key": "fiscal_mexico",
"name": "Facturación Electrónica México",
"version": "1.0.0",
"kernel": ">=2.0.0 <3.0.0"
}| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | Regex ^[a-z][a-z0-9_]{1,63}$. Globally unique. Defines the Postgres schema addon_<key> and the route namespace /m/<key>. |
name | string | yes | Display name. |
description | string | no | Short description, shown in the marketplace card. |
version | string | yes | Strict semver. |
category | string | no | One of integration, utility, finance, crm, operations, ai. |
kernel | string | recommended | Semver range the host kernel must satisfy. Empty = legacy. |
author, website, license | string | no | Marketplace metadata. |
icon_type, icon_slug, icon_color | string | no | Triplet for richer rendering. icon_type: "lucide", "brand" (simple-icons), or "url". |
2. Tenant isolation
"tenant_isolation": "shared"| Value | Behaviour |
|---|---|
"shared" (default) | Single schema addon_<key>, organization_id column + Postgres RLS. |
"schema-per-tenant" | One schema per installation (addon_<key>_<orgshort>), created on install and dropped on uninstall. Use for regulated data. |
"database-per-tenant" | Reserved for future use. |
Empty is treated as shared for backwards compatibility.
3. model_definitions[]
Each entry is materialized as CREATE TABLE addon_<key>.<table_name>.
"model_definitions": [{
"table_name": "tickets",
"model_key": "tickets",
"label": "Tickets",
"org_scoped": true,
"soft_delete": true,
"columns": [
{ "name": "title", "type": "string", "size": 255, "required": true },
{ "name": "status", "type": "string", "size": 20, "default": "'open'", "index": true },
{ "name": "total", "type": "decimal", "default": 0 },
{ "name": "opened_at", "type": "timestamp", "default": "now()" }
]
}]Column types
| Type | Postgres | Notes |
|---|---|---|
string | varchar(<size>) | size required, max 10485760. |
text | text | Unbounded. |
uuid | uuid | |
int | integer | |
bigint | bigint | |
decimal | numeric | |
bool | boolean | |
timestamp | timestamptz | Always with timezone. |
jsonb | jsonb |
Column options
| Field | Type | Meaning |
|---|---|---|
required | bool | NOT NULL constraint. |
index | bool | Creates a btree index. |
unique | bool | UNIQUE constraint. |
default | any | See whitelist below. |
ref | string | Foreign key target. "orders" or "addon_tickets.comments". |
default whitelist
default goes raw into the DDL. Only these literals pass validation:
| Form | Example |
|---|---|
| Numeric | 42, -3, 3.14 |
| Quoted string | "'open'", "'es-MX'" (no embedded ', ", ;, \) |
| Builtin call | "now()", "gen_random_uuid()", "uuid_generate_v4()", "current_timestamp" |
| Boolean / null | true, false, "null" |
Anything else (including arbitrary SQL) is rejected by metacore validate.
Key regex
Every user-supplied identifier (key, model_key, table_name, column name) must match ^[a-z][a-z0-9_]{1,63}$. This blocks both SQL injection and Postgres quoting ambiguities.
4. navigation[]
"navigation": [{
"title": "sidebar.tickets",
"icon": "Ticket",
"target": "sidebar.operations",
"items": [{
"title": "sidebar.tickets.board",
"url": "/m/tickets",
"icon": "Kanban",
"model": "tickets"
}]
}]target(optional): id of an existing core sidebar group. When it matches, items are merged in; otherwise a new group is created.model: when present, the host knows the route is dynamic CRUD on that table. No frontend code is required.
5. actions{} (UI-triggered)
"actions": {
"tickets": [{
"key": "resolve",
"label": "Resolve",
"confirm": true,
"requiresState": ["open", "in_progress"],
"fields": [
{ "name": "note", "type": "text", "required": true }
]
}]
}Executed as POST /api/models/tickets/:id/actions/resolve. The host dispatches to a webhook declared in hooks, a WASM export, or a compiled ActionInterceptor. modal: "custom_slug" opens a custom frontend modal.
6. tools[] (LLM-triggered)
Semantic counterpart to actions. Conversational hosts register these in their agent-tool registry on install.
"tools": [{
"id": "create_order",
"name": "Crear pedido",
"description": "Crea un pedido cuando el cliente expresa intención de comprar. NO llamar para cotizaciones.",
"category": "action",
"endpoint": "/webhooks/create_order",
"method": "POST",
"input_schema": [
{ "name": "product_sku", "type": "string", "required": true,
"extraction_hint": "Código tipo SKU-123 o nombre del producto",
"normalize": "uppercase" },
{ "name": "quantity", "type": "number", "default_value": "1",
"extraction_hint": "Si el cliente dice 'una' o 'un par' inferir 1 o 2" }
],
"trigger_keywords": ["pedido", "comprar", "quiero"],
"trigger_intents": ["order.create"],
"timeout": 15
}]| Field | Purpose |
|---|---|
description | Prompt the LLM sees. Be specific; include negative cases. |
trigger_keywords / trigger_intents | Hints for the routing layer. |
input_schema[i].extraction_hint | Natural-language instruction for the LLM when extracting that field. |
input_schema[i].normalize | Post-extraction transform: uppercase, lowercase, trim, phone_e164. |
input_schema[i].validation | Regex the value must match after normalization. |
cache_ttl | Seconds; non-zero marks the tool as idempotent-GET-like. |
7. capabilities[]
Sandboxed permissions the addon requests. Enforced at runtime by kernel/security/context.go. See capabilities.md for the full kind catalog and validation rules.
"capabilities": [
{ "kind": "db:read", "target": "users", "reason": "Display author names" },
{ "kind": "http:fetch", "target": "api.factura.com", "reason": "Timbrar CFDI" },
{ "kind": "event:emit", "target": "fiscal.stamped" }
]The addon's own schema (addon_<key>.*) is always accessible — never declare it.
8. hooks{} and lifecycle_hooks{}
"hooks": {
"tickets::resolve": "/webhooks/resolve_ticket"
}hooks:"<model>::<action>" → <webhook path or URL>. The host POSTs an HMAC-signed envelope (see addon-publishing.md).lifecycle_hooks: per-model CRUD triggers:jsonTarget types:"lifecycle_hooks": { "tickets": [ { "event": "after_create", "target": { "type": "webhook", "url": "/webhooks/ticket_created" }, "async": true } ] }webhook,wasm_call,agent_task.
9. settings[]
Per-installation configurable values. Stored by the host in metacore_installations.settings.
"settings": [
{ "key": "slack_webhook", "label": "Slack webhook", "type": "text", "secret": true },
{ "key": "default_locale", "label": "Locale",
"type": "select",
"default_value": "es-MX",
"options": [
{ "value": "es-MX", "label": "Español (México)" },
{ "value": "en-US", "label": "English (US)" }
] }
]secret: true ensures the value never leaves the server on GETs and is stored in the secrets manager when the host supports it.
10. frontend{}
"frontend": {
"entry": "https://cdn.example.com/addons/tickets@1.0.0/remoteEntry.js",
"format": "federation",
"expose": "./plugin",
"container": "metacore_tickets",
"integrity": "sha384-..."
}| Field | Meaning |
|---|---|
entry | URL or relative path of remoteEntry.js. |
format | "federation" (recommended) or "script" (legacy window global). |
expose | Federation module name to import (e.g. ./plugin). |
container | Global container name. Must match the name option of @originjs/vite-plugin-federation. Default: metacore_<key>. |
integrity | Optional SRI hash. |
11. backend{}
"backend": {
"runtime": "wasm",
"entry": "backend/backend.wasm",
"exports": ["resolve_ticket", "ping"],
"memory_limit_mb": 64,
"timeout_ms": 10000
}| Runtime | Behaviour |
|---|---|
"webhook" (default) | Hooks dispatch as outbound HMAC-signed HTTP. |
"wasm" | Sandboxed in-process module per wasm-abi.md. |
"binary" | Reserved. |
12. signature{}
Stamped by the marketplace at publish time. Contains developer_id, algorithm (ed25519), digest (sha256 of the bundle), signature value and per-file checksums. Addons never author this block; it is produced by metacore sign and verified by the host on install.
13. events[]
List of topic names the addon will publish. Hosts with an event bus register the schema; subscribers declare capabilities: [{kind: "event:subscribe"}].
"events": ["ticket.created", "ticket.resolved"]See also
dynamic-ui.md— how the SDK turns metadata derived from this manifest into a working CRUD UI.addon-cookbook.md— recipes for foreign keys, soft delete, custom actions, events and more.capabilities.md— full catalog ofkindvalues and target patterns.quickstart.md— hands-on walkthrough end-to-end.