Product module shape
Overview
A product module owns product-specific data projection and browser behavior. It does not own the portal host, provider authorization, token handling, shared assets, or security headers. The module exposes its UI through Go contracts so the host can mount it, pass runtime context, publish metadata, and run deterministic fixture tests.
Contract
A compact module keeps three concerns separate. Provider adapters convert API DTOs and provider errors into safe product facts. View-model builders derive visible state from those facts, permissions, route input, and request context. UI framework pages compose shared Bus UI primitives from the view model and declare the browser hooks that may project events back into Go.
The module still implements the host-facing portal.Module interface from the
bus-portal/pkg/portal package. A GX-ready module also implements
portal.FrameworkModule when it can publish Go-first framework pages,
deterministic render roots, optional Go/WASM runtime metadata, public runtime
configuration, provider origins, same-origin assets, and browser event hooks
through the UIFramework contract.
// Excerpt from package portal.
type Module interface {
ID() string
Title() string
State() ModuleState
DefaultEnabled() bool
NavItems() []NavItem
Handler() http.Handler
}
type FrameworkModule interface {
UIFramework() UIFramework
}
Use an HTTP handler when the page can render from request data and ordinary form submissions. Add a Go WebAssembly runtime only when the page needs browser-owned state, retained callbacks, streaming updates, drag-and-drop, terminal input, or another live behavior that cannot be expressed as deterministic server render plus projected events.
The module layout can be compact:
internal/provideradapter/ provider DTO and public error projection
internal/viewmodel/ screen-ready state, permission display, and copy
internal/ui/ Go and GX framework pages, handlers, and event helpers
Framework pages should take ordinary Go inputs. Data reaches GX through Go values, function arguments, typed props, and host context; it does not require a YAML/JSON descriptor, binding map, or string selector grammar.
package notesui
import (
"github.com/busdk/bus-gx/pkg/gx"
. "github.com/busdk/bus-ui/pkg/uiportal"
)
type NotesPageProps struct {
Host HostContext
Notes []NoteRow
}
func NotesPage(props NotesPageProps) gx.Node {
return (
<PortalShell title="Notes" hostContext={props.Host}>
<NoteTable rows={props.Notes}></NoteTable>
</PortalShell>
)
}
Consequence
Projection tests, component composition, and host routing can evolve in separate layers. Product modules should not mix provider authority, rendering, and browser runtime wiring in one package.
The separation is correct when provider authorization is asserted in provider
or API tests, view-model tests run without rendering HTML, RenderFrameworkPage
can compare stable server HTML from a fixed model, and ProjectFrameworkEvent
can prove that only declared event fields are projected.