Control
Configure the shared tab-container shell that unifies Launchpad and Audit under one navigable UI.
Launchpad (Forms-based flows) and Audit (change history) used to be separate, unrelated web applications with no shared navigation. The Control module removes that split: it owns the shared HTTP context and session that Launchpad, Audit, and Forms' own direct-link flows all run inside, and it serves a single container page with a top-level tab bar that switches between them.
Use the Control module when you want to:
- present Launchpad and Audit (and future tab-based applications) behind one URL and one navigation bar
- control which tabs a session sees, in which order, and which tab opens by default
- restrict a tab to specific roles once a user is authenticated, independently of whether anonymous visitors can see it at all
- require login before the container is shown at all
- send users to a configured page after they log out from the container
Runtime structure
Section titled “Runtime structure”At runtime, the Control module is configured in config/modules/shell.json. Because it creates the shared HTTP context first, every other module meant to run inside the same tab container — Launchpad's Portal module, the Audit module, Forms' own module — must be configured with the same http_listener and http_context values so they join that context instead of creating their own.
The module serves three things:
- the container's own UI bundle, mounted at the shared context's root
- the tab manifest, at
GET <http_context>/api/tabs - the container's logout endpoint, at
GET <http_context>/api/logout(see Logout)
The frontend fetches the tab manifest once when the container loads. The response lists only the tabs the current session is allowed to see, already ordered, together with which tab id is the default:
{ "success": true, "type": "tabs", "data": { "tabs": [ { "id": "launchpad", "label": "Launchpad" } ], "default": "launchpad" }}Each tab entry also includes its context_path relative to the container, for example "launchpad", when the location of the tab's module is known (see Properties). The frontend resolves it against the container's own address, so it keeps working behind a reverse proxy that exposes the container under a different path. A tab whose module is mounted somewhere other than its default, for example Launchpad moved with Forms' launchpad.path, must be given the same path as context_path, or the tab cannot reach its module.
When the session is authenticated, the response also includes user_info (username, display_name) and logout_location (<http_context>/api/logout), so the container's shared header does not need a separate call to get that information. default is null in the rare case where no configured tab is allowed for the current session.
Requiring login for the whole container
Section titled “Requiring login for the whole container”By default (public set to true on the module), anonymous visitors can open the container and see the tabs marked as public. Set the module's own public to false to require login before anything is shown. The tab manifest then answers a session that is not logged in with HTTP 403 and the login URL, and the frontend redirects to login before any tab is loaded:
{ "success": false, "reason": "Unauthorized", "data": { "location": "/control/authn/login" }}After login, the user returns to the container, including any deep link into a tab.
Tab visibility
Section titled “Tab visibility”Each entry in tabs is controlled by two independent settings, not one:
publicdecides whether a session that is not logged in can see the tab at all.requires_any_role/requires_all_rolesdecide which roles can see the tab once logged in.
A tab's own public only matters when the module itself is public. When the module has public set to false, no anonymous session gets past the login requirement, so tab-level public has no effect.
A tab with neither setting still requires authentication — it is not automatically open to anonymous visitors. This matches how role checks already work for Forms flows: "no role requirement configured" means "any authenticated session", not "everyone, including anonymous sessions".
Translations and overlays
Section titled “Translations and overlays”Tabs are rendered inside the container's own page, not as separate applications. The container therefore loads ui_config_overrides.json and customer translation files through the Control module, not through the module behind each tab. Three settings decide whether those files are found.
| Setting | Where | What it does |
|---|---|---|
overlay_dir |
Each module's own configuration (Control and Forms separately) | Directory, or list of directories, searched in order for a requested file before the module's own files. The first match wins. |
additional_resource_paths |
Control module | URL path prefixes, besides assets/, that the container serves from its overlay. |
localesMergePath |
ui_config_overrides.json, normally in the overlay |
Where the browser fetches customer translation files, relative to the application's own address. |
localesMergePath resolves differently depending on which application asks. With "localesMergePath": "language/custom_locales/":
| Application | Address | Request |
|---|---|---|
| The container (Launchpad and Audit tabs) | /control/ |
/control/language/custom_locales/sv.json |
| Standalone Launchpad | /control/launchpad/ |
/control/launchpad/language/custom_locales/sv.json |
| A flow | /control/<flow>/ |
/control/<flow>/language/custom_locales/sv.json |
Standalone Launchpad and flows serve any path under their own address. The container shares its address with the other modules on the same context, so it only serves the root page, top-level files and assets/** by default, plus the prefixes listed in additional_resource_paths. A request outside those paths gets 404 Resource not found, even when the file exists in the overlay.
Requirements
Section titled “Requirements”For translations to work in the container:
- The Control module's
overlay_dirlists the same directory, or directories, as the Forms module's. localesMergePathpoints underassets/, for exampleassets/custom_locales/, or its first path segment is listed inadditional_resource_paths, for examplelanguageforlanguage/custom_locales/.- The translation files exist at that path in the overlay, for example
language/custom_locales/sv.jsonanden.json.
Files served through additional_resource_paths are sent with Cache-Control: no-cache and read from disk on every request, so edited translations take effect without a restart. Files under assets/ are cached by the server, and by browsers for up to a day, so after editing them restart Control and expect users to see the change only once their cached copy expires.
Example (Management Center)
Section titled “Example (Management Center)”Forms and Control modules:
"overlay_dir": "${system.customer_home}/overlay/"Control module, when translations live outside assets/:
"additional_resource_paths": ["language"]Overlay:
${system.customer_home}/overlay/├── assets/│ └── ui_config_overrides.json ← "localesMergePath": "language/custom_locales/"└── language/ └── custom_locales/ ├── en.json └── sv.jsonFlow names
Section titled “Flow names”Launchpad uses a flow's display_name as a translation key, and shows it unchanged when no translation exists. Either keep the display name as text and translate it, for example "User Self-Registration": "Registrera dig som ny användare", or use a dedicated key. Category names are translated the same way. Avoid colons (:) in display_name.
Verifying
Section titled “Verifying”Open /control/ with the browser's developer tools on the network tab and caching disabled. The request for <localesMergePath><language>.json under /control/ must return 200, and translated flow names must show immediately, the same as in /control/launchpad/.
If flow names only become translated after opening a flow, the container is not reaching the translation file: the flow loads its own copy of the translations, which then applies to the whole page. Check the requirements above.
Logout
Section titled “Logout”The container's header logs out through GET <http_context>/api/logout. That endpoint stores the module's logout_endpoint_url as the session's post-logout destination and then redirects to the authenticator's logout. After the session has ended, the authenticator sends the user to that destination.
If the Control module has no logout_endpoint_url, the destination stored by Launchpad or a flow is used instead. That is the Forms module's logout_endpoint_url. If none of them has a value, the authenticator's own logout_location is used. To send users to the same place whichever view they log out from, set logout_endpoint_url to the same value on both the Control and the Forms module, for example ${globals.logout_endpoint_url}.
Related
Section titled “Related”In this section