Most screens of the web app come from metadata, but some apps need custom screens: dashboards, boards, builders, and home widgets. That code does not ship with the public web app. Each such app has one JavaScript bundle in gated_apps/<app>/app.js at the repository root, served only to a user whose company may run the app. This page explains how a bundle is declared, fetched, loaded, and given access to platform functions. Licensing and entitlement are described on app store, licensing, and entitlements.
In short¶
- There are 9 bundles:
analytics,bom,calendars,intranet,inventory,nesting,procurement,production, andprojects(counted withls gated_apps/*/app.js). Their manifests carrygated_entry: true(src/cluster_erp/modules/app_store/catalog.json). - The bundles live outside the
/appstatic mount and are served byGET /api/v1/app_store/assets/{app_key}/{filename}(src/cluster_erp/modules/app_store/routes.py:94-155). - The client fetches a bundle as a blob, imports it from an object URL, and calls its
widgets(ctx)export (webapp/js/custom-widgets.js:134-157). - A bundle has no imports. Every platform function it uses arrives in
ctx, thePLATFORM_CTXobject with 42 distinct members (webapp/js/custom-widgets.js:50-121, counted from the object's keys;iconis listed twice).
How it works¶
Declaration¶
loadAppCatalog() reads the registry and maps every module of every app with gated_entry to that app's key with setGatedApps (webapp/js/home.js:452-457, webapp/js/custom-widgets.js:44-48). The custom-widget registry starts empty; every custom widget comes from a bundle (webapp/js/custom-widgets.js:40-42).
| Bundle | Lines | Manifest |
|---|---|---|
gated_apps/analytics/app.js |
4,253 | src/cluster_erp/modules/app_store/catalog.json:705-757 |
gated_apps/bom/app.js |
3,774 | src/cluster_erp/modules/app_store/catalog.json:808-854 |
gated_apps/calendars/app.js |
1,020 | src/cluster_erp/modules/app_store/catalog.json:609-622 |
gated_apps/intranet/app.js |
1,409 | src/cluster_erp/modules/app_store/catalog.json:1195-1241 |
gated_apps/inventory/app.js |
172 | src/cluster_erp/modules/app_store/catalog.json:462-508 |
gated_apps/nesting/app.js |
280 | src/cluster_erp/modules/app_store/catalog.json:657-670 |
gated_apps/procurement/app.js |
1,657 | src/cluster_erp/modules/app_store/catalog.json:953-999 |
gated_apps/production/app.js |
1,282 | src/cluster_erp/modules/app_store/catalog.json:560-606 |
gated_apps/projects/app.js |
1,037 | src/cluster_erp/modules/app_store/catalog.json:317-363 |
Line counts were made with wc -l. The fleet app declares gated_entry: false and has no bundle (src/cluster_erp/modules/app_store/catalog.json:511-557).
Loading¶
The sequence diagram shows the first use of a bundle.
sequenceDiagram participant V as module-app.js or gen-detail.js participant R as custom-widgets.js participant S as app_store routes V->>R: ensureModuleWidgets(module) R->>S: GET /app_store/assets/key/app.js S-->>R: bundle or 403 R->>R: Blob, createObjectURL, import R->>R: mod.widgets(PLATFORM_CTX) R->>R: REGISTRY[module.key] = factory V->>R: getCustomWidget(module, key) R-->>V: render function
In words:
- A module app, a custom detail tab, or a module home widget calls
ensureModuleWidgets(moduleName)before it builds its tabs or paints (webapp/js/module-app.js:79-81,webapp/js/gen-detail.js:372-379,webapp/js/home.js:255-258). - If the module belongs to a gated app, the client fetches
/app_store/assets/<app_key>/app.jswith the session cookie as a blob (webapp/js/custom-widgets.js:134-141). - The text is wrapped in a JavaScript blob, imported from an object URL, and the URL is revoked afterwards (
webapp/js/custom-widgets.js:142-152). - The bundle's default export provides
widgets(ctx), which returns a map from widget name to factory. Each factory is stored under<module>.<name>(webapp/js/custom-widgets.js:145-149,gated_apps/inventory/app.js:1-6). getCustomWidget(module, key)calls the factory with the module name and returns the render function (webapp/js/custom-widgets.js:159-162).
Each bundle is loaded once per session; concurrent callers share one load (webapp/js/custom-widgets.js:123, webapp/js/custom-widgets.js:137-156). Logout clears the loaded set so the next sign-in fetches freshly deployed code (webapp/js/custom-widgets.js:125-132).
Server route¶
The asset route requires a signed-in user. It serves the file only when the app is installed, licensed for the company or marked as a system app, and assigned to one of the user's roles; otherwise it answers 403 with code entitlement.asset_denied (src/cluster_erp/modules/app_store/routes.py:103-130). It serves only plain file names inside the gated_apps folder and answers 404 for anything else (src/cluster_erp/modules/app_store/routes.py:131-144). Responses carry Cache-Control: no-cache and an ETag, so the entitlement check runs on every request and an unchanged bundle costs a 304 (src/cluster_erp/modules/app_store/routes.py:145-155).
The content security policy allows scripts from blob: for this import (src/cluster_erp/server/app.py:1325-1338 at commit 92f8051).
Platform context¶
PLATFORM_CTX is the whole surface a bundle may use. The comment states that it is extended deliberately and doubles as the contract's documentation (webapp/js/custom-widgets.js:50-51).
| Area | Members |
|---|---|
| API and data | apiFetch, apiFetchBlob, apiUpload, apiErrorText, GenApi, fkItems, fkPreload, fkInvalidate, resolveMissingLabels |
| Metadata | metaEntity, resolveLabel, providerOf, hasPermission |
| Generic engines | makeGenDetail, makeGenList, makeGenSettings, openActionSheet |
| Navigation | pushView, popView |
| Data signal | emitDataChanged, watchData, dataSeq, canPaint |
| Language and format | getLang, t, formatDate, formatDateTime, formatDateTimeShort, getTimezone, COMMON_TIMEZONES |
| Interface helpers | esc, safeUrl, showToast, confirmAlert, bindLongPress, createTouchGate, selectInput, icon, a11y, getTheme |
| Widget scope | resolveScope, scopeSegmentHtml, wireScope, noEmployeeHtml |
Source: webapp/js/custom-widgets.js:52-121.
Why it is built this way¶
- The comment in
src/cluster_erp/modules/app_store/routes.py:94-98states that an unlicensed or unseated tenant's browser never receives a byte of the module's code: the registry does not list the app and the route refuses it. - The comment in
webapp/js/custom-widgets.js:7-13states that blob imports cannot resolve relative paths, which is why every dependency comes throughctx. - The comment in
webapp/js/custom-widgets.js:125-129states that keeping bundles across a re-login left users on old code after a deploy.
Rules and boundaries¶
- A home widget declared in metadata must be registered in its module's bundle.
tests/test_metadata_driven_rules.py:158checks this for everyHomeWidgetMeta. - A missing widget is never shown as a broken tab: the module app skips the section, a detail tab shows
common.no_results, and an unlicensed owner showserror.entitlement.module_not_installed(webapp/js/module-app.js:126-137,webapp/js/gen-detail.js:375-391,webapp/js/home.js:259-276). - Bundles follow the same data-changed signal and touch gate as the shell; see data refresh and change signals and home screen, dock, and launcher.
Interaction with other parts¶
- The app catalogue and the layers behind installed, licensed, and seat-assigned are on app store module.
- Custom sections inside module apps are described on module apps and app scope.
Limits and open points¶
- If a bundle fetch or import fails, the rejected promise stays in the loaded set, so later calls in the same session fail at once until logout (
webapp/js/custom-widgets.js:137-156). - The header comment and the fetch agree: the entry is fetched through the entitlement-checked asset route with the session cookie by
apiFetchBlob(webapp/js/custom-widgets.js:7-11,webapp/js/api.js:151-152).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| gated bundle | gated_apps/<app>/app.js |
The custom code of one licensed app, served only to entitled users. | kapılı paket | src/cluster_erp/modules/app_store/routes.py:100 |
| platform context | PLATFORM_CTX |
The object of platform functions passed to every gated bundle. | platform bağlamı | webapp/js/custom-widgets.js:52 |
| custom widget | getCustomWidget |
A render function registered by a bundle under <module>.<name>. |
özel bileşen | webapp/js/custom-widgets.js:159 |
Related pages¶
- Web app platform
- App store, licensing, and entitlements
- Module apps and app scope
- Home widgets
- App store module
Verified against commit 92f8051 on 2026-11-13.