Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Web app

Explanation

Gated app bundles

How custom screens of licensed apps ship as gated_apps bundles, reach the browser through the app store asset route, and register widgets.

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, and projects (counted with ls gated_apps/*/app.js). Their manifests carry gated_entry: true (src/cluster_erp/modules/app_store/catalog.json).
  • The bundles live outside the /app static mount and are served by GET /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, the PLATFORM_CTX object with 42 distinct members (webapp/js/custom-widgets.js:50-121, counted from the object's keys; icon is 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:

  1. 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).
  2. If the module belongs to a gated app, the client fetches /app_store/assets/<app_key>/app.js with the session cookie as a blob (webapp/js/custom-widgets.js:134-141).
  3. 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).
  4. 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).
  5. 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-98 states 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-13 states that blob imports cannot resolve relative paths, which is why every dependency comes through ctx.
  • The comment in webapp/js/custom-widgets.js:125-129 states 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:158 checks this for every HomeWidgetMeta.
  • 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 shows error.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

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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13