Service Frontend Contract
A service frontend is a Module Federation remote. It must satisfy both a TypeScript export contract (consumed by the shell) and a Go HTTP contract (consumed by the shell's service tracker).
TypeScript: ServiceModule interface
The shell loads each service via loadRemote('{name}/index') and validates the result. The remote's index entrypoint must export:
// web/shell/src/lib/remotes.ts
export interface ServiceModule {
default: (props: { connected: boolean }) => any;
manifest: { name: string; label: string; route: string; minWidth?: number };
SidebarContent?: () => any;
HeaderActions?: () => any;
}
default and manifest are required. SidebarContent and HeaderActions are optional slots rendered by the shell's layout.
connected semantics
connected is derived from the shell's ServiceTracker, not from the remote itself.
- HTTP-only service (
WSPathsempty):connectedistruewhenever the health probe returns 200,falsewhen the service is unreachable. - WebSocket service (
WSPathsnon-empty):connectedstartsfalseon discovery and becomestrueonly while at least one WS client is connected. The health probe does not affectconnectedfor WS services. The tracker maintains a ref count viaOnConnect/OnDisconnect.
manifest must match the Go-side Manifest
name, label, and route must be identical to the values the Go binary declares in its frontend.Manifest. The shell reads those fields from the health probe; ServiceModule.manifest is used by the shell's routing and layout. A mismatch causes undefined behavior.
minWidth is TypeScript-only and has no Go equivalent.
MF shared singletons
The shell declares the following shared modules in its vite.config.ts:
// web/shell/vite.config.ts
shared: {
'solid-js': { singleton: true, eager: true },
'@workfort/ui': { singleton: true, eager: true },
'@workfort/ui-solid': { singleton: true, eager: true },
'@workfort/auth': { singleton: true, eager: true },
}
Remotes that use SolidJS must declare solid-js, @workfort/ui, @workfort/ui-solid, and @workfort/auth as shared with import: false (consume from shell, do not bundle their own copy).
Remotes that do not use SolidJS should only declare @workfort/ui and @workfort/auth as shared. Framework adapter libraries are bundled by the remote.
@solidjs/router is not shared. MF's dev-mode virtual module generator uses require() to detect named exports, which fails for ESM-only packages. Remotes receive routing context through props, not through a shared router instance.
Go: frontend.Manifest and frontend.Handler
Manifest struct
// pkg/frontend/frontend.go
type Manifest struct {
Name string `json:"name"`
Label string `json:"label"`
Route string `json:"route"`
WSPaths []string `json:"ws_paths,omitempty"`
}
WSPaths controls the connected semantics described above. An empty or omitted WSPaths marks the service as HTTP-only.
Handler(fsys, manifest)
func Handler(fsys fs.FS, m Manifest) http.Handler
Mounts the following routes under /ui/:
| Route | Behavior |
|---|---|
GET /ui/health | 200 + manifest JSON if remoteEntry.js exists; 503 + manifest JSON otherwise |
/ui/assets/* | Cache-Control: public, max-age=31536000, immutable |
/ui/* (everything else) | Cache-Control: no-cache |
fsys must be rooted at the Vite build output directory. Pass the result of fs.Sub(embedFS, "web/dist") or equivalent — Handler opens files relative to that root.
The remoteEntry.js existence check runs once when Handler is called at server startup. It does not re-check at request time. The health status is immutable for the lifetime of the process.
Both 200 and 503 responses include the full manifest JSON body. The tracker uses both status codes: 200 means the UI is built and available (hasUI = true), 503 means the service is reachable but the UI is not built (hasUI = false). The presence of ws_paths in either response determines whether connected is WS-driven.
Shell rendering: ServiceMount
The shell renders each service through ServiceMount, which layers four behaviors:
ErrorBoundary— catches any error fromloadRemoteor the remote component itself and renders<Unavailable>.Suspense— renders<wf-skeleton width="100%" height="200px">while the remote module is loading.- Warning banner — if the module has loaded but
connectedisfalse, renders a<wf-banner variant="warning">instead of the component. The banner message tells the user the service is starting up or temporarily unavailable and will update automatically. <Dynamic>— once the module is loaded andconnectedistrue, rendersmod().defaultwith{ connected }as props.
The condition for the warning banner is !(mod() || props.connected): the banner shows only when both the module is absent and connected is false. Once the module has loaded, it is rendered regardless of connected — the component receives the live connected value as a prop and handles it internally.