skip to content
docsspec
github

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.

  1. 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: true also places a launchpad tile that opens the console, and ui.tiles[] places several entry points into it instead. (An admin can still register apps by hand under settings → apps.)
  2. 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 module import(), and the proxy also guards against SSRF by only accepting in-cluster targets.
  3. The shell fetches the addon's federated bundle through that proxy, mounts its exported component, and passes it the host props below.

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 fetchesMeaning
embed/assets/remoteEntry.jsThe federation entry of your embed build
module ./ConsoleExposed by your federation config
named export ConsoleA default export is rejected — the host looks for Console
embed/assets/console.cssYour 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 section target must 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.

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.