Integration Recipes — Custom-Edition Default#
The composition root (
internal/app/app.goRun) is shared platform: config, database, identity, content engine, media stack, plugin system, boot. An edition is a fixed module list compiled into one binary (blog, core, commerce live ininternal/editions/<name>/as separate packages so a binary links only its modules). Platform code never imports a theme or the engine directly — the edition owns that.
Platform vs edition — where does the wiring go?#
| Change | Goes in |
|---|---|
| Business rule, domain type, sentinel, repository port | modules/<name>/domain/ |
| HTTP handler, seam interface | modules/<name>/api/ (+ agent/ for the Bearer realm) |
| SQL adapter | modules/<name>/store/ (one impl over *sql.DB + kernel.Dialect) |
| External provider client | modules/<name>/infrastructure/ (domain port) or modules/<name>/<driver>/ (self-contained protocol driver, e.g. payments/midtrans); never in domain/ |
| Cross-context adapter (module A needs module B’s service) | internal/editions/<name>/ — the only layer that may import both sides. One-seam-per-file, named for the module fed (cart.go, orders.go); wire<Module> steps stay in the edition’s main file, never mixed into adapter files |
Service construction, module New + candidates, server boot | internal/app (Run) + the edition’s Wire/Mount hooks |
| Private module wiring | A new custom edition (internal/editions/<custom>/ + cmd/<custom>/main.go). Never internal/app by default |
First-party upstreaming (editing internal/app, SUBMODULES, the
release EDGE map, docs/built-in-modules.md) is maintainer-gated and
out of scope for private modules.
Recipe: new private module + custom edition#
Assumes a clean checkout at a known tag (SKILL.md Step 2). Module key
mymod, edition mycustom.
1. Scaffold the module#
Follow scaffold/INSTRUCTIONS.txt: copy the core tier + the overlays
the design needs into modules/mymod/. Resolve __MODULE_NAME__ and
__KERNEL_VERSION__ (derived from the checkout’s pinned sibling
require, e.g. awk '/lesstruct\/kernel / {print $2}' modules/cart/go.mod).
No replace directives in modules/mymod/go.mod.
2. Register the workspace#
| |
Private modules stay out of the release EDGE map and
docs/built-in-modules.md (first-party surfaces only).
3. Write the custom edition#
Create internal/editions/mycustom/mycustom.go:
| |
Theme swap (when the edition needs a different embedded theme instead
of the default): follow blog.EditionWithTheme — pass the alternate
kernel.ThemeProvider into the constructor rather than registering two
providers (first-provider-wins).
Cross-context seams (module A needs module B’s service) become
small adapters in internal/editions/mycustom/ (e.g. mymod.go):
fields + seam methods in the interface’s declaration order + the New*
constructor. wire steps never live in adapter files.
4. Add the thin main#
Create cmd/mycustom/main.go (mirrors main.go /
cmd/lesstruct-core/main.go — stays thin, no wiring):
| |
5. Wire order reference (what Run does with your edition)#
- Platform services built (config, DB, identity, content, media…).
edition.Wire(core)— your modules appended tocore.Candidates(platform modules register around it; order only pins the init log and migration sequence — the registry sorts topologically anyway).ApplyDisabledprunes[modules] disabled+ cascade (warns); only kept modules register.InitAll→ capability discovery (Capabilities[T]) → firstThemeProviderinstalls → module migrations run →edition.Mount.
6. Verify both workspaces#
| |
GOWORK=off failing while in-workspace passes means a missing
require or a stray replace — fix go.mod, never add a filesystem
replace (it breaks go install pkg@version; local checkouts resolve
through go.work).
7. Serve + cascade check#
| |
Then confirm the cascade: [modules] disabled = ["mymod"] prunes the
module (plus transitive dependents) with a warning, and its routes
unmount where flag-gated. A kept module whose hard dep is absent still
fails startup fast — that path is unchanged.