Skip to content

Announcing Metacore v3 kernel v0.20

The manifest is the contract. Code is an implementation detail.

Module Contract v3 is the biggest leap in how you describe an addon since Metacore shipped. One strict, declarative JSON document now drives the schema, the API, the permissions, the UI surface, the catalog listing and the regional scoping — and the kernel runs it end to end. This page is the tour.

TL;DR

Declare apiVersion: asteby.com/v3, and you get: a typed model contract, capability + RBAC security, declarative action modals and line-items, installable vertical Presets, federated bespoke UI, catalog i18n and country scoping — with dual-read v2 so nothing breaks while you migrate.

Why v3

v2 made the manifest describe an addon. v3 makes the manifest be the addon. Three principles drive it:

  1. Manifest is the contract. Every capability the kernel grants, every event it routes, every table it materialises and every UI slot it renders is declared. If it's not in the manifest, it doesn't exist — the kernel never introspects your binary to discover behaviour.
  2. Coupling through typed events and named slots — never imports. Two unrelated vendors ship addons that compose, because they meet at a published slot_kind or event name, not a Go/TS import path.
  3. Tenant isolation is shared + Postgres RLS by default. The right default for ~95% of addons; schema-per-tenant is an opt-in escape hatch.

What's in the box

CapabilityWhat it gives youSince
Manifest v3 contractStrict, schema-validated apiVersion/kind/metadata/compatibility/models/contributions/rbacv0.13
Dual-read v2Legacy flat manifests still install — zero-break migrationv0.13
Action modalsDeclarative fields / custom modal / confirm — rich action UI, no frontend codev0.14
Frontend federationBespoke modals and full pages federated into the hostv0.14
Settings & column extrasSetting.description, Setting.type: "number", Column.comment, compiled handlersv0.15
Line-itemsRepeatable group as a declarative action field (invoice lines, order items)v0.16
Presetskind: "Preset" — install a whole vertical as one unitv0.17
WASM action triggersDeclared wasm handlers validate without a backend blockv0.18
Catalog i18nmetadata.i18n — localize the marketplace listing (es/en)v0.19
Country scopingmetadata.countries — scope an addon to regionsv0.20
Currency hookPer-org currency resolved at INSERT, geography-agnostic USD fallbackunreleased

Everything below is verifiable against kernel v0.20 and the docs/spec/v3 contract.

The contract, at a glance

jsonc
{
  "apiVersion": "asteby.com/v3",
  "kind":       "Addon",
  "metadata":        { "key": "inventory", "version": "1.0.0", "i18n": {}, "countries": ["MX","CO"] },
  "compatibility":   { "requires": [ { "key": "kernel", "version": ">=3.0.0 <4.0.0" } ] },
  "tenancy":         { "isolation": "shared", "rls_column": "organization_id" },
  "capabilities":    [ { "kind": "db:write", "target": "addon_inventory.*" } ],
  "models":          [ { "key": "Product", "table": "products", "columns": [] } ],
  "contributions":   { "navigation": [], "slots": [], "actions": [], "subscriptions": [] },
  "extension_points":{ "events": [], "slot_kinds": [], "model_extensions_accepted": [] },
  "rbac":            { "roles": [], "permissions": [] },
  "settings":        [ { "key": "low_stock_threshold", "type": "number", "description": "…" } ]
}

Before & after

The same addon, the v2 way and the v3 way. v3 trades a flat grab-bag for a typed, sectioned contract — and unlocks everything else on this page.

json
{
  "key": "tickets",
  "name": "Tickets",
  "version": "1.0.0",
  "kernel": ">=2.0.0 <3.0.0",
  "tenant_isolation": "shared",
  "model_definitions": [
    {
      "table_name": "tickets", "model_key": "tickets",
      "org_scoped": true, "soft_delete": true,
      "columns": [
        { "name": "title",  "type": "string", "size": 255, "required": true },
        { "name": "status", "type": "string", "size": 20, "default": "'open'" }
      ]
    }
  ],
  "capabilities": [ { "kind": "db:write", "target": "tickets" } ]
}
json
{
  "apiVersion": "asteby.com/v3",
  "kind": "Addon",
  "metadata": { "key": "tickets", "name": "Tickets", "version": "1.0.0" },
  "compatibility": { "requires": [ { "key": "kernel", "version": ">=3.0.0 <4.0.0" } ] },
  "tenancy": { "isolation": "shared", "rls_column": "organization_id" },
  "capabilities": [ { "kind": "db:write", "target": "addon_tickets.*" } ],
  "models": [
    {
      "key": "Ticket", "table": "tickets",
      "columns": [
        { "name": "id",     "type": "uuid", "primary_key": true, "default": "gen_random_uuid()" },
        { "name": "organization_id", "type": "uuid", "not_null": true },
        { "name": "title",  "type": "text", "not_null": true },
        { "name": "status", "type": "text", "default": "'open'" }
      ]
    }
  ],
  "rbac": {
    "permissions": [ { "key": "tickets.write", "label": "Manage tickets" } ],
    "roles": [ { "key": "tickets_agent", "permissions": ["tickets.write"] } ]
  }
}

Presets — install a vertical as one unit

kind: "Preset" bundles a curated set of addons with sane defaults. Install one, and the kernel resolves and installs every preset.addons[] entry in dependency order, then applies the preset's defaults on top. This is how a whole vertical ships as a single click.

json
{
  "apiVersion": "asteby.com/v3",
  "kind": "Preset",
  "metadata": { "key": "retail_starter", "name": "Retail Starter", "version": "1.0.0" },
  "preset": {
    "addons": [
      { "key": "core_catalog", "version": "^1.2.0" },
      { "key": "inventory",    "version": "^1.0.0" },
      { "key": "pos_terminal", "version": "^1.0.0" },
      { "key": "reporting_basic", "version": "^1.0.0", "optional": true }
    ],
    "defaults": {
      "inventory.low_stock_threshold": 5,
      "pos_terminal.default_currency": "MXN"
    }
  }
}

Action modals & line-items — rich UI, zero frontend

An action declares how it collects input. The runtime renders the right UI for free:

  • confirm — a yes/no prompt.
  • fields — an auto-generated modal with typed inputs.
  • modal — a custom, federated frontend modal referenced by slug.

Fields can include line-items: a repeatable group rendered as add/remove rows — invoice lines, order items, anything tabular.

json
{
  "contributions": {
    "actions": [
      {
        "key": "create_invoice",
        "label": "New invoice",
        "target_model": "Customer",
        "fields": [
          { "name": "due_date", "type": "date", "required": true },
          {
            "name": "lines", "type": "line_items",
            "fields": [
              { "name": "description", "type": "text", "required": true },
              { "name": "qty",         "type": "number", "default": 1 },
              { "name": "unit_price",  "type": "number", "required": true }
            ]
          }
        ],
        "handler": { "type": "wasm", "function": "CreateInvoice" }
      }
    ]
  }
}

The kernel mounts POST /api/dynamic/customers/:id/actions/create_invoice; the SDK renders the modal with a repeatable line-item table. You write the handler body — nothing else.

Catalog i18n & country scoping

The marketplace listing speaks the user's language and respects geography, declared in metadata:

json
"metadata": {
  "key": "facturacion",
  "name": "Electronic Invoicing",
  "countries": ["MX", "CO", "AR"],
  "i18n": {
    "es": { "name": "Facturación Electrónica", "description": "Timbrado y CFDI." },
    "en": { "name": "Electronic Invoicing", "description": "Stamping and tax receipts." }
  }
}

Locale-specific by config, not by hardcode

Fiscal identifiers, address formats and currency are org config, never baked into the kernel or an addon. metadata.countries scopes availability; the per-org currency hook resolves money at write time with a geography-agnostic USD fallback.

Backwards compatible by design

Migrate without a rewrite

Kernel 3.x is dual-read. It accepts both v2 manifests (no apiVersion) and v3, transparently up-converting v2 into the v3 in-memory shape — so existing addons keep installing and running while you move over at your own pace. Kernel 4.x removes v2. Author new addons in v3 today.

The full field-by-field v2 → v3 mapping is in the kernel's migration guide.

Get started

Build your first v3 addon

  1. Read the Manifest concept — the v3 contract in depth.
  2. Follow Build an addon — scaffold, declare, install.
  3. Spin up a full app: npm create @asteby/metacore-app my-app -- --example fullstack-starter.
  4. Browse the SDK suite — every package that renders v3.

Metacore is open-source. Apache-2.0.