Solid client runtime
Status: accepted for the client-route migration pilot.
Decision
Use Templ, HTMX, and Alpine for server-owned pages. Use ordinary Solid TSX for routes whose interaction state is client-owned. A DOM subtree has exactly one renderer owner: do not mount Solid inside an HTMX swap target or mount React and Solid into the same root.
The server remains authoritative for persisted data, RBAC, tenant identity, and business validation. Solid owns transient interaction state such as an editable draft, selection, pending input, and local preview. Route metadata and RPC contracts describe access and data, not the UI. There is no screen DSL, schema-driven layout, or feature-local host.
Go-owned feature API
Keep a Solid screen beside its owning Go presentation code and declare the connection once:
type Props struct {
ProductID string `json:"productId"`
}
var Editor = solid.Import[Props](
"./ProductEditor.tsx",
solid.WithRPC(
solid.Query[LoadRequest, LoadResponse]("products.load", solid.Cacheable(2)),
solid.Action[SaveRequest, SaveResponse]("products.save", solid.Invalidates("products.load")),
),
)Use application.ClientRoute("products.configure") on the existing Go route,
set clienthost.Route.Feature to Editor, and return
RoutePayload{Screen: Editor.Render(props)}. The import is declarative and does
no I/O at package initialization.
Run iota-solid before frontend typecheck/build. It writes
ProductEditor.solid.generated.ts beside the component and one generated lazy
catalog in the frontend workspace. Both files are committed and checked with
iota-solid -check. The generated module exports Props, RPC,
rpcContracts, and a default component whose signature is checked against the
Go props. iota-solid -watch reconciles add, move, and delete changes in dev.
The standard catalog host owns lazy loading, safe bundle errors, Retry, mount, and dispose. A feature owns only the states inside its mounted screen. Adding a second feature must not require a handwritten central registry, mount function, asset tag, or TypeScript route table.
Public boundaries
application.ClientRoute(routeID)declares a generated Solid feature route; the imported descriptor supplies its stable feature identity. New client routes also declarePublic,Authenticated,RequireAll, orRequireAnyexplicitly.pkg/clienthostrenders the authenticated shell fragment and versioned JSON bootstrap. It carries SDK identity, route identity, session/CSRF state, locale messages, permissions, theme, and service endpoints.@iota-uz/sdk/solidcontains the Solid mount, host services, typed RPC primitives, managed streaming subscriptions, and the small draft primitive.@iota-uz/sdk/styles.csspublishes the same token source used by server components. Features may compose normal CSS and Solid components but must not introduce a competing global reset, font, palette, or theme model.
@iota-uz/sdk/client-host remains available for existing React consumers
during migration. React renderer metadata is accepted only as compatibility;
new routes use the generic client renderer. The two renderers never share a
mounted subtree.
Lifecycle and failure semantics
The Solid host validates bootstrap protocol plus Go/npm/source identity before rendering. Mount fails if another renderer already owns the root. Dispose is idempotent and releases the Solid owner, session-independent host subscriptions, and dirty navigation listeners.
Queries cancel the previous reactive request and ignore late results. Actions
never retry implicitly and expose typed field errors. createDraft deep-clones
the server snapshot, tracks nested changes, supports reset, and accepts a save
response only when no newer edit occurred. A newer server snapshot is not
merged into a dirty draft; the feature owns that merge policy.
useManagedStream subscribes to one authenticated SSE stream per route
lifetime through the host: session/CSRF headers and typed 401 re-auth, bounded
reconnect with jitter, heartbeat stale detection, Last-Event-ID/cursor
replay, and one deduplicated terminal event. The subscription closes with the
owning Solid root, so route unmount cancels the request and releases
resources. Stream events fold into feature state through one callback; the
contract may declare RPC cache families to invalidate, but never a universal
merge policy. Lifecycle states are exposed as a signal and to host telemetry
under iota.stream.<state> with the bounded stream identity.
Deliberate exclusions
This runtime does not contain product or insurance code, a universal state framework, optimistic rollback policy, streaming cache merge, or generated screen structure. It generates data contracts and the lazy catalog, never UI layout. Full locale TOML generation/validation and replacement of the remaining React features are separate follow-up work.