Development setup
This guide is for contributors to metacore-kernel. If you are embedding the kernel into an application, read consumer-guide.md instead.
Table of contents
- Prerequisites
- Clone and bootstrap
- Repository layout
- Tests, vet, race detector
- Working with the SDK locally
- Branching and contribution flow
- Style and architectural guardrails
- Debugging the WASM runtime
- Releasing
Embedding the kernel in a host? You want
consumer-guide.mdandembedding-quickstart.md. Working on the dynamic CRUD framework? Readdynamic-system.mdfirst — fixtures, handler tests, and migration helpers indynamic/*_test.go,metadata/*_test.go, andinstaller/*_test.goassume the contracts documented there.
1. Prerequisites
| Tool | Version |
|---|---|
| Go | 1.25+ (matches go.mod and CI) |
| Git | 2.30+ |
| GitHub CLI | recommended for PRs and releases |
| Make | optional |
The kernel depends on wazero (pure Go WASM runtime — no cgo), Fiber v2, GORM, gofiber/websocket, prometheus/client_golang, and goose for versioned migrations. There are no native build tools required.
2. Clone and bootstrap
go env -w GOPRIVATE="github.com/asteby/*"
git config --global url."git@github.com:".insteadOf "https://github.com/"
mkdir -p ~/projects && cd ~/projects
git clone git@github.com:asteby/metacore-kernel.git
git clone git@github.com:asteby/metacore-sdk.git
cd metacore-kernel
go mod downloadThe two repos must live as siblings — the kernel's go.mod carries replace github.com/asteby/metacore-sdk => ../metacore-sdk so SDK changes are picked up without publishing a tag.
Verify the toolchain:
go version # expect 1.25.x
go vet ./...
go test ./...3. Repository layout
metacore-kernel/
├── auth/ JWT, login/refresh handlers, Fiber middleware
├── bridge/ Adapters: kernel actions/tools/webhooks ↔ host integrations
├── bundle/ Addon bundle I/O (`bundle.tgz` reader/writer)
├── docs/ Developer-facing documentation (this directory)
├── dynamic/ Generic CRUD over registered models
├── eventlog/ Org-scoped persisted event log with cursor pagination
├── events/ In-process pub/sub bus for addons
├── flow/ Workflow primitives reused by addons
├── host/ `App` and `Host` facades
├── httpx/ HTTP helpers shared across handlers
├── installer/ Install/enable/disable/uninstall flow
├── lifecycle/ Addon contract, registry, interceptors
├── log/ Builder-style logger (legacy; use obs/ for new code)
├── manifest/ Declarative addon manifest schema
├── metadata/ TableMetadata/ModalMetadata registry, cache, handler
├── metrics/ Prometheus integration
├── migrations/ Goose-based versioned migration runner
├── modelbase/ Stable interfaces and base structs
├── navigation/ Sidebar merger
├── notifications/ Delivery queue + workers + ChannelHandler
├── obs/ Structured slog logger with request-id propagation
├── permission/ Role + capability checks
├── push/ Web Push (VAPID)
├── query/ Filter/sort/paginate query builder
├── runtime/wasm/ wazero-based WASM runtime
├── security/ Enforcer, Capabilities, HMAC, secretbox, nonce
├── strings/ Shared string helpers
├── tool/ Addon tool runtime + dispatcher + registry
├── webhooks/ Outbound HMAC-signed webhooks with retry
├── ws/ WebSocket hub
├── ARCHITECTURE.md The four laws of the kernel — read before adding a package
├── CHANGELOG.md Release history
└── README.md Top-level overviewEach package owns its tests (*_test.go), a doc.go where useful, and a single coherent responsibility (see ARCHITECTURE.md, Law 0).
4. Tests, vet, race detector
CI runs the same commands you should run locally before opening a PR (.github/workflows/ci.yml):
go vet ./...
go test -race -coverprofile=coverage.out ./...Useful subset patterns:
# A single package, verbose
go test -race -v ./runtime/wasm/...
# Watch a single test
go test -race -run TestEnforcer_Shadow ./security/...
# Coverage HTML
go tool cover -html=coverage.outThe race detector is mandatory — the kernel hosts long-lived goroutines (WS hub, webhook dispatcher, notification workers) and most regressions surface only under -race.
5. Working with the SDK locally
During day-to-day work the kernel resolves the SDK from ../metacore-sdk via the replace directive in go.mod. To preview the build that consumers will actually pull:
go mod edit -dropreplace github.com/asteby/metacore-sdk
go mod tidy
go test ./...Re-add the replace before resuming local work:
go mod edit -replace github.com/asteby/metacore-sdk=../metacore-sdk
go mod tidyNever commit a go.mod without the replace directive on a feature branch — the release script drops it as part of tagging.
6. Branching and contribution flow
- Branch from
main. Use a descriptive prefix:feat/,fix/,refactor/,docs/,chore/. - Conventional Commits are enforced for changelog generation. The
feat:/fix:/BREAKING CHANGE:markers drive the SemVer decision at release time (seerelease.md). - One coherent change per PR. Public-API changes need a corresponding
// Deprecated:comment if they replace existing symbols. - Open the PR, let CI go green, request review, squash-merge.
7. Style and architectural guardrails
The full statement is in ARCHITECTURE.md. The four points to internalize before contributing:
- Stability by interfaces, not structs. Every public contract lives behind an interface (
AuthUser,AuthOrg,ModelDefiner, …). Apps extend by composition; the kernel evolves without breaking them. - Opinionated defaults, pluggable escape hatches. Constructors take a
Config; behavior overrides areWith*methods, never forks. - Services are mandatory, handlers are optional. A
service.gomust never importgithub.com/gofiber/fiber/v2. Handlers are thin Fiber wrappers around services. - What belongs in the kernel. Substrate that every web app needs on day one. Optional reusable infra goes in the SDK; product-specific code goes in the app. When in doubt, default to keeping it out.
Additional dependency rules:
modelbase/imports nothing beyondgorm.io/gorm,github.com/google/uuid,golang.org/x/crypto/bcrypt. No Fiber, no HTTP, no SDK.obs/imports only the standard library. It is the most upstream package.- No kernel package may import
github.com/asteby/metacore-sdk/pkg/*unless the dependency is on a stable public type (manifest, bundle schema).
8. Debugging the WASM runtime
The WASM runtime lives in runtime/wasm/. A handful of patterns that pay off when diagnosing addon failures:
- Reproduce in
wasm_test.go. The package ships a fixture that compiles a tiny Go-to-WASM module and runs it through the full ABI; copy that test and add the failing scenario. - Inspect host imports.
capabilities.goregisters every host import. If the addon'simported function not founderrors at instantiation time, the symbol name is missing fromregisterHostModule. - Check the enforcer mode. Locally the default is
ModeShadow; turn onModeEnforce(METACORE_ENFORCE=1) when chasing capability bugs so they surface as errors instead of warnings. - Memory and timeouts. Defaults are 64 MiB / 10 s per invocation, with a global 256 MiB ceiling on the runtime config. Override per-addon via
manifest.BackendSpec.
9. Working on the dynamic CRUD framework
Most kernel changes that affect consumer apps land in dynamic/, metadata/, permission/, installer/, or manifest/. A few patterns that pay off when iterating there:
- End-to-end fixture per package.
dynamic/service_test.gobuilds an in-memory SQLite DB, registers a fake model, and exercises Create / Get / List / Update / Delete. Copy that fixture to add a regression test for a new code path; do not stand up Postgres unless you are testing RLS or a Postgres-only feature. - Handler tests use
app.Test().metadata/handler_test.goshows the pattern: build a Fiber app, mount the handler, sendhttptest-style requests, assert the JSON envelope. Keep handler tests as thin as possible — service-level tests cover correctness, handler tests cover status codes and the wire envelope. - Manifest fixtures live in tests.
manifest/validate_test.goandinstaller/dualwrite_test.godeclare manifests inline. There is no separate fixture directory; if you need a complex one, add it next to the test that uses it. - Schema-affecting changes touch three places. Adding a column type to
dynamic/model.go:columnGoTypealso requiresdynamic/schema.go:pgColumnTypeand a corresponding entry indynamic-system.mdAllowed column types. Renaming or removing a column type is a MAJOR bump because addon manifests in the wild depend on it. - Public response shapes are wire contracts. The JSON tags on
modelbase.TableMetadata,modelbase.ModalMetadata, thedynamic.Handlerenvelope ({success, data, meta}) andquery.PageMetaare stable across minors. Adding a field is fine; removing or renaming one is a MAJOR.
10. Releasing
The release process — version selection, tag publication, GoReleaser, consumer dispatch, retract — is documented end-to-end in release.md. In short: git push origin vX.Y.Z runs the release workflow, which runs the test suite, indexes the proxy, publishes a GitHub Release and notifies every consumer repository.