Secure WASM runtime and substrate for declarative, multi-tenant Go applications.
📚 Documentation: Full docs live at asteby.github.io/metacore. The
docs/folder in this repo is kept for offline reference only.
- What is
metacore-kernel - Dynamic CRUD framework
- Architecture
- Embedding
- Subsystems
- WebSocket protocol
- Addon contract
- Documentation
- Development
- Release process
- Module path
- License
metacore-kernel is the runtime substrate that every Metacore application
embeds as a Go module. It owns the cross-cutting concerns no individual product
should re-implement:
- WASM addon runtime — compiles and executes signed addons in a wazero sandbox with memory caps, wall-clock timeouts, and a host module that gates every privileged syscall through a capability enforcer.
- Capability-based security — HMAC-signed install secrets, nonce-based
replay protection, and a global enforcer with
shadow/enforcemodes that operators flip at runtime. - Lifecycle orchestration — install / enable / disable / uninstall hooks,
plus a
Boot()phase that wires shared services (DB, event bus, …) into every registered addon. - Metadata-driven CRUD — a registry of
TableMetadata/ModalMetadatafeeds a generic dynamic CRUD service that every host shares; apps register models, the kernel exposes the routes. - Event bus, WebSocket hub, push, webhooks — the technical communication primitives every multi-tenant web application eventually needs, framework- agnostic at the service layer and Fiber-native at the handler layer.
The kernel is embeddable, not a server. It compiles into the host
binary; there is no metacore daemon. All public contracts are behind
interfaces, so adding fields to base structs is a non-breaking change
(see ARCHITECTURE.md for the four-laws statement).
metacore-kernel consumes the public metacore-sdk
for manifest, bundle and dynamic-table type definitions; it does not
publish addons of its own. Addons are authored against the SDK CLI
(metacore build / compile-wasm) and executed by this kernel.
Zero-glue CRUD. Declare a model in
manifest.json, install the addon, get a working API and admin UI. No handlers, no migrations, no per-model React.
When an addon ships model_definitions[] in its manifest, installer.Install
creates the Postgres schema, runs DDL, registers metadata, and the kernel's
dynamic.Handler starts serving live CRUD endpoints — all without addon code:
{
"key": "tickets",
"model_definitions": [{
"table_name": "tickets",
"org_scoped": true,
"soft_delete": true,
"columns": [
{ "name": "subject", "type": "string", "size": 200, "required": true, "index": true },
{ "name": "status", "type": "string", "size": 24, "default": "'open'" },
{ "name": "priority", "type": "string", "size": 12 },
{ "name": "due_at", "type": "timestamp" }
]
}],
"capabilities": [
{ "kind": "db:read", "target": "addon_tickets.*" },
{ "kind": "db:write", "target": "addon_tickets.*" }
]
}That manifest is the only code needed to expose:
| Endpoint | Behaviour |
|---|---|
GET/POST /api/dynamic/tickets |
Paginated list, filters, sort, search · create |
GET/PUT/DELETE /api/dynamic/tickets/:id |
Get, update (load-merge-save), soft delete |
GET /api/options/tickets |
Select/lookup options (with resolver) |
GET /api/search/tickets |
Full-text search over searchable columns |
GET /api/metadata/table/tickets |
TableMetadata for the runtime-react DynamicTable |
GET /api/metadata/modal/tickets |
ModalMetadata for the runtime-react form generator |
Per-request user capabilities (tickets.read, tickets.create, …) are
gated by permission.Service; addon-level capabilities (db:write addon_tickets.*) are gated by security.Enforcer in either shadow or
enforce mode.
Read docs/dynamic-system.md for the full
end-to-end walkthrough,
docs/dynamic-api.md for the HTTP reference, and
docs/permissions.md for the capability model.
┌──────────────────────────────────────────────┐
│ Host application │
│ │
│ fiber.App ──► host.App.Mount(/api) │
└──────┬───────────────────────────────────────┘
│ embeds via go.mod
▼
┌──────────────────────────────────────────────┐
│ metacore-kernel (this repo) │
│ │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ host/ │──►│ auth · permission │ │
│ │ App + Host │ │ metadata · dynamic│ │
│ │ facades │ │ query · obs │ │
│ └──────┬──────┘ └─────────────────────┘ │
│ │ │
│ ┌──────▼──────┐ ┌─────────────────────┐ │
│ │ lifecycle/ │──►│ events/ (bus) │ │
│ │ installer/ │ │ eventlog/ (log) │ │
│ │ navigation/ │ │ ws/ push/ │ │
│ │ manifest/ │ │ notifications/ │ │
│ └──────┬──────┘ │ webhooks/ │ │
│ │ └─────────────────────┘ │
│ ┌──────▼──────┐ ┌─────────────────────┐ │
│ │ runtime/ │──►│ security/ │ │
│ │ wasm/ │ │ Enforcer · HMAC │ │
│ │ (wazero) │ │ Capabilities │ │
│ └──────┬──────┘ └─────────────────────┘ │
└─────────┼────────────────────────────────────┘
│ host module imports (gated)
▼
┌──────────────────────────────────────────────┐
│ Addon WASM module │
│ built with metacore-sdk CLI · sandboxed │
│ exports: alloc(i32) · <fn>(ptr,len) i64 │
└──────────────────────────────────────────────┘
manifest.json (model_definitions[], capabilities[])
│
▼
installer.Install
├──► dynamic.EnsureSchema (CREATE SCHEMA addon_<key>)
├──► dynamic.Apply (versioned SQL migrations)
├──► dynamic.CreateTable (CREATE TABLE + RLS policy)
├──► dynamic.SyncSchema (ADD COLUMN IF NOT EXISTS)
└──► lifecycle OnInstall/OnEnable
│
▼
modelbase.Register("<model>", factory) ◄── host wires this
│
▼
host.App.Mount
├──► metadata.Handler ──► GET /metadata/{table,modal,all}/:model
└──► dynamic.Handler ──► GET/POST/PUT/DELETE /dynamic/:model
GET /options/:model · /search/:model
│ ▲
│ │ permission.Service.Check (per request)
▼
@asteby/metacore-runtime-react renders the table + modal from metadata
Two transports talk to the frontend at the same time:
- WebSocket (
ws/) — bidirectional, primary channel for live updates, notifications, addon events. - REST — Fiber routes mounted under
/api/*for auth, metadata, CRUD, navigation, push subscriptions, webhooks, addon installs.
Every privileged operation an addon attempts (DB read, event publish, HTTP
fetch, …) is brokered by the security.Enforcer. The enforcer can run in
shadow mode (log only) during rollout and is flipped to enforce via the
METACORE_ENFORCE environment variable without redeploys.
The kernel exposes two facades. Pick the highest-level one that fits.
host.App wires auth + metadata + dynamic CRUD + WebSocket hub and,
optionally, permission, push, webhooks and Prometheus metrics. It is the
single call most apps need.
package main
import (
"log"
"os"
"github.com/gofiber/fiber/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/asteby/metacore-kernel/host"
"github.com/asteby/metacore-kernel/modelbase"
)
func main() {
db, err := gorm.Open(postgres.Open(os.Getenv("DATABASE_URL")), &gorm.Config{})
if err != nil {
log.Fatalf("db: %v", err)
}
app := host.NewApp(host.AppConfig{
DB: db,
JWTSecret: []byte(os.Getenv("JWT_SECRET")),
RunMigrations: true,
EnableMetrics: true,
EnableWebhooks: true,
}).RegisterModel("products", func() modelbase.ModelDefiner {
return &Product{}
})
fiberApp := fiber.New()
app.Mount(fiberApp.Group("/api"))
log.Fatal(fiberApp.Listen(":3000"))
}Mount returns the authenticated sub-router, so the app can layer its own
domain handlers on top of the kernel-provided ones.
When the binary needs to host WASM addons (install, enable, lifecycle
hooks, navigation merge), construct a host.Host instead. This is the mode
host applications use to load addons such as conversational extensions or
marketplace integrations.
h, err := host.New(host.Config{
DB: db,
KernelVersion: "0.7.2",
Services: map[string]any{
"eventbus": bus,
},
})
if err != nil { log.Fatal(err) }
h.RegisterCompiled("billing", &billing.Addon{})
if err := h.Boot(); err != nil { log.Fatal(err) }The two facades compose: real apps build a host.App for HTTP plumbing and a
host.Host for the addon plane, sharing the same *gorm.DB.
For a step-by-step walk-through see
docs/CONSUMER_GUIDE.md.
| Package | Responsibility |
|---|---|
host/ |
App and Host facades — boot orchestration and Fiber mount |
auth/ |
JWT issue/verify, password hashing, login/refresh handlers, Fiber middleware |
permission/ |
Role + capability checks; pluggable PermissionStore |
metadata/ |
TableMetadata / ModalMetadata registry + cache + handler |
dynamic/ |
Generic CRUD over registered models, options/search resolvers |
query/ |
Filter/sort/paginate query builder |
modelbase/ |
Stable interfaces (AuthUser, AuthOrg, ModelDefiner) + base structs |
obs/ |
Structured slog logger with request-id propagation |
ws/ |
WebSocket hub, per-user routing, broadcast helpers |
push/ |
Web Push (VAPID) subscriptions and dispatch |
webhooks/ |
Outbound HMAC-signed webhooks with retry queue |
notifications/ |
Delivery queue, dedup, retry, pluggable ChannelHandler |
eventlog/ |
Org-scoped persisted event log with cursor pagination |
events/ |
In-process pub/sub bus for addons (capability-checked, wildcard patterns) |
lifecycle/ |
Addon contract (Manifest, OnInstall, …) + registry + interceptors |
installer/ |
Install / enable / disable / uninstall flow + frontend bundle materialization |
navigation/ |
Merge core sidebar groups with addon contributions |
manifest/ |
Declarative addon manifest schema (mirrored by SDK) |
bundle/ |
Addon bundle I/O contracts (bundle.tgz reader/writer) |
tool/ |
Addon tool runtime + dispatcher + registry |
bridge/ |
Adapters that map kernel actions/tools/webhooks to host integrations |
runtime/wasm/ |
wazero-based WASM runtime, ABI, capability-gated host imports |
security/ |
Enforcer, Capabilities, HMAC, secretbox, nonce store, webhook dispatch |
metrics/ |
Prometheus registry, Fiber middleware, /metrics handler |
migrations/ |
Versioned SQL migration runner (Goose) for kernel-owned tables |
httpx/ |
HTTP helpers reused across handlers |
log/ |
Builder-style logger (legacy; new code uses obs/) |
A package-by-package contract — what belongs in the kernel versus the SDK
versus an app — is documented in ARCHITECTURE.md.
Clients connect to wss://<host>/api/ws?token=<jwt>. The hub propagates the
authenticated user_id from Locals into the connection and registers a
per-user fan-out. Messages are JSON envelopes:
{ "type": "NOTIFICATION", "payload": { /* … */ } }Standard message types live in ws/hub.go (MsgNotification,
MsgStatusUpdate, MsgCustom). Apps can declare their own constants — the
type is a plain string, no fork required.
For org-wide broadcasts, callers query their own DB for user IDs and call
hub.SendToUsers(ids, msg). Notification persistence is delegated to the
optional OnNotification hook so the hub stays ORM-free.
Addons are authored against metacore-sdk
and executed by this kernel. The split is intentional:
- SDK (public, npm + Go CLI) owns the developer experience:
metacore init,metacore validate,metacore build,metacore sign,metacore compile-wasm, the manifest schema, the bundle layout, the TypeScript types for runtime React. - Kernel (this repo) owns the runtime: parses the manifest, verifies signatures, materialises bundles on disk, instantiates the WASM module, brokers every host import, runs lifecycle hooks, merges navigation, exposes installer endpoints.
The wire-level WASM ABI is defined in runtime/wasm/abi.go and is documented
on the SDK side at docs/wasm-abi.md (single source of truth — the kernel is
the implementer, the SDK is the reference).
| Document | Audience |
|---|---|
ARCHITECTURE.md |
Maintainers — the four laws of the kernel |
docs/dynamic-system.md |
App teams using the dynamic CRUD framework |
docs/dynamic-api.md |
Anyone calling the dynamic / metadata HTTP endpoints |
docs/permissions.md |
Auth / capability model — user gates and addon gates |
docs/embedding-quickstart.md |
First-time hosts — 10-minute walkthrough |
docs/CONSUMER_GUIDE.md |
App teams embedding the kernel (long form) |
docs/dev-setup.md |
Contributors working on the kernel itself |
docs/RELEASE.md |
Release manager and consumers |
docs/consumer-renovate-template.json |
Drop-in Renovate config for consumers |
CHANGELOG.md |
Anyone consuming a new tag |
git clone git@github.com:asteby/metacore-kernel.git
cd metacore-kernel
# Tests with the race detector — same flags CI runs.
go test -race ./...
# Static analysis.
go vet ./...
# Single package, verbose.
go test -race -v ./runtime/wasm/...Local development assumes the SDK is checked out as a sibling directory:
~/projects/metacore-sdk
~/projects/metacore-kernel
go.mod carries a replace github.com/asteby/metacore-sdk => ../metacore-sdk
directive so SDK changes are picked up without publishing a tag. Drop the
replace before tagging a release — see docs/dev-setup.md.
Releases are tag-driven. git push origin vX.Y.Z triggers
.github/workflows/release.yml, which runs the test suite, pings the Go
proxy, publishes a GitHub Release via GoReleaser, and dispatches a
metacore-kernel-released event to every consumer repository so Renovate runs
on demand instead of waiting for the next cron tick.
The full procedure — version selection, pre-releases, retract, troubleshooting
— is in docs/RELEASE.md.
github.com/asteby/metacore-kernel
Resolves through the public Go module proxy. See
docs/CONSUMER_GUIDE.md for the step-by-step
embedding walk-through.
Licensed under the Apache License, Version 2.0.