Hosted consoles — a full addon UI inside the portal
An addon with real administrative surface (queues, settings, dashboards) gets a console: a React app the portal shell hosts in-page. The addon needs no origin, no Ingress/Route, and no session of its own — the portal proxies to it in-cluster and mounts its UI in the shell's React tree.
Remotes are attached at runtime, not declared at build time, because which apps exist is a registry decision. Adding a console never requires rebuilding or releasing the portal.
How hosting works
Section titled “How hosting works”- An app is registered in the portal (key, title, and a proxy url — the
addon's in-cluster address, e.g.
http://acquire). Installing the addon does this from its manifest:ui.console: truealso places a launchpad tile that opens the console, andui.tiles[]places several entry points into it instead. (An admin can still register apps by hand under settings → apps.) - The portal serves
/portal/app/{key}and reverse-proxies/api/portal/apps/{key}/*to the proxy url. The proxy is deliberately the front door: a browser cannot attach a bearer token to a moduleimport(), and the proxy also guards against SSRF by only accepting in-cluster targets. - The shell fetches the addon's federated bundle through that proxy, mounts its exported component, and passes it the host props below.
The contract your console must satisfy
Section titled “The contract your console must satisfy”Your UI is a module federation
remote, built with @originjs/vite-plugin-federation (format: 'esm'). The
host expects, relative to your service's HTTP root:
| The host fetches | Meaning |
|---|---|
embed/assets/remoteEntry.js | The federation entry of your embed build |
module ./Console | Exposed by your federation config |
named export Console | A default export is rejected — the host looks for Console |
embed/assets/console.css | Your styles; loaded alongside the module |
Shared dependencies: react and react-dom are shared singletons at
^18.3.0. Your embed build must accept that version range or your console
will not mount.
The host renders:
<Console apiBase={string} token={string | undefined} onUnauthorized={() => void} />apiBase— the base URL for your own API through the portal proxy (/api/portal/apps/{key}/). Call your backend via this base and every request carries the user's bearer.token— the current access token, when you need it directly (e.g. an SSE fetch-stream).onUnauthorized— call it when your API answers 401; the shell owns the re-auth flow.
Failures are contained: the host wraps your console in an error boundary, so a broken remote degrades to an error card rather than taking the shell down.
🧭 This contract is currently defined by the host implementation (
zaentrum-portal/src/apps/AppHost.tsx) and this page. A versioned, importable types package is roadmap; until then, treat this page as the contract and expect it to change only with a note in the release notes.
The console is where configuration is edited
Section titled “The console is where configuration is edited”An addon's settings are edited in its own console, never in the portal — the addon stores them, validates them and keeps its secrets (configuration). The portal's part is to say what is missing and send the admin to the right view:
- When the manifest declares
setup, settings → addons shows a checklist per addon, and each section's configure opens/portal/app/{key}{target}. Every sectiontargetmust therefore be a view your console routes — the same rule as a tile target — and it should land on exactly the form that section is about, not on a landing page. - The console mounts there with the same host props as anywhere else. Nothing
about setup is passed in: the view loads its state from your API through
apiBase, like every other view. - Settings views are admin views. Hide them from non-admins if you like, but the API is what enforces the role.
- Secret inputs are write-only:
type="password", never prefilled, cleared after save, with a stored badge when a value is set.
After a save, the checklist reflects the change the next time settings → addons asks the addon's setup endpoint; the portal holds no state to update.
Worked example
Section titled “Worked example”acquire ships its console this way: one
Vite embed build in the addon image, zero Ingress objects in its manifests, and
the portal tile opens /portal/app/acquire.