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:
- 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.
- Coupling through typed events and named slots — never imports. Two unrelated vendors ship addons that compose, because they meet at a published
slot_kindor event name, not a Go/TS import path. - 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
| Capability | What it gives you | Since |
|---|---|---|
| Manifest v3 contract | Strict, schema-validated apiVersion/kind/metadata/compatibility/models/contributions/rbac | v0.13 |
| Dual-read v2 | Legacy flat manifests still install — zero-break migration | v0.13 |
| Action modals | Declarative fields / custom modal / confirm — rich action UI, no frontend code | v0.14 |
| Frontend federation | Bespoke modals and full pages federated into the host | v0.14 |
| Settings & column extras | Setting.description, Setting.type: "number", Column.comment, compiled handlers | v0.15 |
| Line-items | Repeatable group as a declarative action field (invoice lines, order items) | v0.16 |
| Presets | kind: "Preset" — install a whole vertical as one unit | v0.17 |
| WASM action triggers | Declared wasm handlers validate without a backend block | v0.18 |
| Catalog i18n | metadata.i18n — localize the marketplace listing (es/en) | v0.19 |
| Country scoping | metadata.countries — scope an addon to regions | v0.20 |
| Currency hook | Per-org currency resolved at INSERT, geography-agnostic USD fallback | unreleased |
Everything below is verifiable against kernel v0.20 and the docs/spec/v3 contract.
The contract, at a glance
{
"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.
{
"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" } ]
}{
"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.
{
"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.
{
"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:
"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
- Read the Manifest concept — the v3 contract in depth.
- Follow Build an addon — scaffold, declare, install.
- Spin up a full app:
npm create @asteby/metacore-app my-app -- --example fullstack-starter. - Browse the SDK suite — every package that renders v3.
- Architecture — how the kernel and SDK fit together.
- Dynamic CRUD · Permissions · Lifecycle