Skip to content

Architecture

Metacore is four layers stacked on top of each other. Each layer has one job and a clean contract with the layer above. Most teams will only care about two of them at a time — the host they're embedding into, and the addon they're building. The rest is what makes it work end-to-end.

The four layers

┌──────────────────────────────────────────────────────────┐
│  Hosts (your application)                   the surface  │
│   └─ React + Vite + @asteby/metacore-runtime-react       │
├──────────────────────────────────────────────────────────┤
│  Host backends (Go)                          the embed   │
│   └─ host.App + host.Host                                │
├──────────────────────────────────────────────────────────┤
│  metacore-kernel                             the runtime │
│   └─ dynamic CRUD · permissions · ws · wasm · lifecycle  │
├──────────────────────────────────────────────────────────┤
│  metacore-sdk + addons                       the contract│
│   └─ manifest.json (v3) · runtime-react · CLI · packages │
└──────────────────────────────────────────────────────────┘

1. The contract — metacore-sdk + addons

The bottom layer is what an addon is. The SDK defines:

  • The manifest schema — the shape of manifest.json, the addon's source of truth. The current contract is Module Contract v3 (apiVersion: asteby.com/v3): metadata, capabilities, models, contributions, extension_points, tenancy, rbac, settings, lifecycle. It also covers non-addon units — kind: Preset (a vertical bundle of addons), Theme and ConnectorPack.
  • The bundle format.mcbundle, a signed tarball with the manifest, optional WASM module, assets, and frontend code.
  • The frontend runtime@asteby/metacore-runtime-react plus sibling packages (auth, theme, ui, i18n, app-providers, websocket, notifications, billing, marketplace, …) that read the same metadata the kernel exposes and render typed UI without bespoke code.
  • The CLI — the Go metacore tool (go install github.com/asteby/metacore-sdk/cli@latest) scaffolds, validates, builds, signs and publishes addons.

An addon is just a directory with a manifest, optional Go code (compiled to WASM), and optional React code that gets registered as a slot. It's the only layer most app builders ever touch.

2. The runtime — metacore-kernel

The kernel is a Go library you embed in a Fiber server. It owns:

  • Dynamic CRUD. A generic store reads the manifest's models[] and serves list / get / create / update / delete over REST. Pagination, sort, filter, relation filters, group-by and aggregations come for free.
  • Permissions. Two layers: capability checks (does the addon have db:write on this table?) and per-user resource permissions (does this user have tickets.create?). Both enforced at every call.
  • Lifecycle. Install, upgrade, uninstall — schema migrations, hook execution, metadata registration. Hot, no restart.
  • WASM sandbox. Addons that run code do it inside wazero. The kernel exposes a small, audited ABI; the addon can't see the host's memory or filesystem.
  • WebSockets. A real-time hub is mounted automatically. Addons emit events; clients subscribe by tenant + channel.

The kernel exposes its own surface as a Go API and as HTTP routes. Hosts choose how much to mount.

3. The embed — host backends

A host backend is a Go binary that imports the kernel and adds whatever is specific to that product: auth, billing, integrations, custom domain endpoints. The kernel provides host.App and host.Host helpers that handle the boilerplate (config, DI, routing, graceful shutdown).

The kernel mounts onto a Fiber router — app.Mount(fiberApp.Group("/api")) plugs the dynamic CRUD, metadata, options and metrics routes under any prefix, alongside your existing routes. A typical main.go is under 60 lines.

4. The surface — host frontends

A host frontend is a Vite + React app that uses @asteby/metacore-runtime-react to render addon UIs. The runtime fetches metadata from the kernel, mounts the right components, and exposes hooks for everything else: queries, mutations, real-time, navigation, slot composition.

A host can take many shapes: an internal operator panel, a customer-facing portal, a marketplace and admin surface, an embedded settings area inside an existing product, a vertical-specific UX. They all consume the same SDK; none of them has any custom logic for any individual addon.

Data flow: declare → CRUD UI

A user opens the Tickets page in a host frontend. Here's what happens:

manifest.json                      ┌──────────────────┐
  models: Ticket                   │  installer       │
  capabilities: db:rw              │  applies DDL,    │
        │                          │  registers meta  │
        ▼                          └────────┬─────────┘
  installed bundle ────────────────────────▶│

   ┌────────────────────────────────────────────────┐
   │  kernel                                        │
   │  GET  /api/metadata/table/tickets        ──────┼──▶ schema
   │  GET  /api/dynamic/tickets?page=1&...     ─────┼──▶ rows
   │  enforces: capability + user permission         │
   └────────┬───────────────────────────────────────┘


   ┌──────────────────────┐
   │  host frontend       │
   │  <DynamicTable>      │
   │   reads meta + rows  │
   │   renders columns,   │
   │   pagination, sort   │
   └──────────────────────┘

A few things are worth pointing out:

  1. There is no per-table code. No TicketsController, no TicketsListPage. The runtime composes both from metadata.
  2. Every call is permissioned. The kernel checks both the addon's capability (declared in the manifest) and the user's resource permission (granted at runtime). A misconfigured addon can't escape its own contract.
  3. Real-time is implicit. The same store that handles writes pushes change events through the WebSocket hub; the SDK's table component subscribes by default.

A custom action

CRUD covers the 80%. The remaining 20% — domain operations, integrations, side effects — comes through contributions.actions[]:

json
{
  "contributions": {
    "actions": [
      {
        "key": "close_with_reason",
        "label": "Close ticket",
        "target_model": "Ticket",
        "confirm": true,
        "fields": [
          { "name": "reason", "type": "text", "required": true }
        ],
        "handler": { "type": "wasm", "function": "CloseWithReason" }
      }
    ]
  }
}

The kernel mounts POST /api/dynamic/tickets/:id/actions/close_with_reason. The runtime renders a button on every row of the <DynamicTable> and — because the action declares fields — an auto-generated modal for the inputs (an action can also point at a custom modal or just a confirm prompt, and inputs can include repeatable line-item groups). The body of the action is yours — Go code inside the addon (compiled to WASM, or registered directly if it's an embedded addon).

The pattern repeats: declare the shape, plug in the behavior, the rest is automatic.

Cross-repo release pipeline

asteby/metacore-sdk                    asteby/metacore-kernel
  ├─ changesets PR                       ├─ feature PR
  ├─ Version Packages PR                 ├─ tag vX.Y.Z
  ├─ npm publish (all packages)          ├─ GoReleaser → GitHub Release
  └─ TypeDoc → Pages                     └─ pkg.go.dev refresh

   ┌────────────────────────────────────────────┴────────┐
   ▼                                                     ▼
 host frontends                                   host backends
 install via pnpm,                                go get -u,
 pickup new SDK in Vite                           rebuild binary

Both repos cut releases independently. The SDK is the noisy one (frontend churn); the kernel is the conservative one (runtime contract). Versions are coordinated when an SDK release requires a kernel feature, but they don't have to ship together.

Where to go next

Metacore is open-source. Apache-2.0.