Skip to content
Moku LabsDocs
Concepts pages

Three layers

createCoreConfig, createCore, createApp. Who writes what.

three-layers.mdllms.txt

The factory chain

Moku is a micro-kernel with three layers. Each layer constrains the one above it, and each step of the chain captures its types in a closure, so the next step stays typed with no explicit generics.

createCoreConfig(id, { config, plugins? })   Step 1: capture Config, Events, core plugins
  → { createPlugin, createCore }
createCore(coreConfig, { plugins, ... })     Step 2: capture the default plugins
  → { createApp, createPlugin }
createApp({ plugins?, config?, ... })        Step 3: merge the app's extras, run init
  → App  (frozen, plugin APIs mounted by name)
LayerWho writes itCanCannot
1. @moku-labs/corethe kernellifecycle, registry, events, config, freezeknow anything about a domain
2. Framework, such as @moku-labs/webthe framework authordefine the config shape, events, default pluginschange the kernel
3. Your appyou, or an agentconfigure, compose plugins, call typed APIschange the framework or bypass a plugin

Layer 1: createCoreConfig

Pure machinery. The framework names itself, declares its global config shape and its event map, and lists the core plugins that land on every ctx. @moku-labs/web calls it with the id web, which is why every kernel error in this framework starts with [web].

Layer 2: createCore

The framework fixes its default plugins. An app can configure them but never remove or reorder them. @moku-labs/web ships site, i18n, router, head and spa as defaults and exports createApp and createPlugin to you.

OptionPurpose
pluginsDefault plugins, always loaded.
pluginConfigsFramework-level overrides.
onReadyRuns after every onInit.
onErrorReceives hook dispatch failures. Lifecycle errors propagate instead.

Layer 3: createApp

This is the layer you write. Everything comes from the framework: createApp, createPlugin and the plugin instances.

src/app.ts
import { createApp, createPlugin } from "@moku-labs/web";

const greetingPlugin = createPlugin("greeting", {
  config: { name: "world" },
  api: ctx => ({
    hello: () => `Hello, ${ctx.config.name}!`
  })
});

const app = createApp({
  plugins: [greetingPlugin],
  config: { mode: "ssg" },
  pluginConfigs: {
    site: { name: "My Site", url: "https://example.com" },
    greeting: { name: "Moku" }
  }
});

console.log(app.greeting.hello()); // "Hello, Moku!"
  1. Added after the framework's defaults.
  2. Per plugin, by name. Checked against the plugin's config type.

The final plugin list is [...frameworkDefaults, ...yourPlugins]. createApp is synchronous: init runs before it returns.

Rules

  • An app never imports @moku-labs/core. It imports createApp, createPlugin and plugins from its framework.
  • Config is shallow. { ...defaults, ...overrides }. A nested object is replaced as a whole, so keep config flat.
  • Everything is frozen. The global config, each plugin config and the app. Only ctx.state is mutable.