Example micro-frontend package that:
- follows the Suncoast UI module contract (
module.definition.json) - includes a host adapter for the current shell
CmsModuleDefinitioncontract - builds to a single browser JS file (
dist/example-mfe.js) - provides Directus seed data for
cms_modules - demonstrates GraphQL async communication:
- submit via GraphQL HTTP mutation
- receive streamed response via GraphQL WS subscription
npm install
npm run devLocal preview URL:
http://localhost:4173/preview/(default)
Build production artifact:
npm run buildBuild output:
dist/example-mfe.js(single-file bundle)dist/example-mfe.js.mapdist/module.definition.json
GraphQL endpoint/token values are runtime-resolved. They are not baked into the built bundle.
Resolution order:
props.graphql.*from CMS block config- Shell runtime values (
#cms-rootdata attrs /window.__SUNCOAST_RUNTIME__.graphql) - Browser auth storage fallback for token (for preview helper flows)
Session lifecycle ownership:
- Token refresh scheduling and session-expiry warning UI are owned by the shell runtime.
- This MFE only consumes shell auth globals (for example
window.__SUNCOAST_AUTH__) before making requests. - For template guidance on logout events and local auth-state cleanup, see
MFE_AUTH_INTEGRATION_REFERENCE.md.
Build metadata debug helper (devtools):
window.__SUNCOAST_GET_MFE_BUILD_INFO__("mfe-internal-operations")- Returns compile-time metadata for the loaded bundle (module version, build version, commit, timestamp, mode).
window.__SUNCOAST_GET_MFE_BUILD_INFO__()returns all loaded MFE build entries by module key.- Lookup is exact-key first, then case-insensitive.
Optional token exchange:
- Configure
graphql.tokenExchangeto exchange the shell bearer token to the audience this MFE needs before GraphQL submit/stream calls. - Preferred: set
graphql.tokenExchange.exchangeUrlto your backend exchange endpoint (for examplehttps://login.suncoast.systems/v1/auth/token-exchange) so the browser never calls Keycloak token exchange directly. - When
exchangeUrlis set, the MFE performs gateway exchange even if the source token already contains the requested audience (so gateway can still mint/augment required claims). - If module security is enabled (
secured=trueorrequiredRoleset) and the shell token lacks Hasura claims, the MFE now auto-attempts gateway token exchange usingexchangeUrl+appSlug(even whentokenExchange.enabledis false). - If
tokenExchange.tokenUrl/tokenExchange.clientIdare omitted, the module falls back to shell auth runtime values (data-auth-token-url,data-auth-client-id,window.__SUNCOAST_AUTH__.config).
Local preview:
- Copy
.env.local.exampleto.env.local - Set optional preview auth values
- Run
npm run dev
Supported env vars:
MFE_PREVIEW_AUTH_GATEWAY_URL(preview login gateway, usuallyhttps://login.suncoast.systems)MFE_PREVIEW_AUTH_APP_SLUG(registered app slug in auth-gateway, for exampleexample-mfe-devfor local/preview andexample-mfe-prodfor production)MFE_PREVIEW_AUTH_CODE_PARAM(query key returned by gateway callback, defaultgateway_code)MFE_PREVIEW_PORT(dev only)MFE_PREVIEW_VM_STATS_BASE_URL(optional, e.g.http://localhost:8787/vm-statsorhttps://vm-stats.internal/vm-stats)MFE_PREVIEW_VM_STATS_SYSTEMS_PATH(optional override for systems endpoint path, default/stats/systems)MFE_PREVIEW_VM_STATS_SNAPSHOT_PATH(optional override for snapshot endpoint path, default/stats/snapshot)MFE_PREVIEW_CLOUD_BILLING_GRAPHQL_ENDPOINT(optional override for billing Hasura GraphQL endpoint, default/graphql)MFE_PREVIEW_CLOUD_BILLING_HASURA_ACTION_NAME(optional override for billing Hasura action field)MFE_PREVIEW_CLOUD_BILLING_HASURA_ACTION_CANDIDATES(optional comma-separated action field candidates)MFE_PREVIEW_CLOUD_BILLING_SERVICES(optional comma-separated billing service IDs to show)
The local preview page (/preview/) now includes a login helper that uses the shared auth-gateway flow:
- Fill
Auth Gateway URLandAuth App Slug(or setMFE_PREVIEW_AUTH_*env vars). - Click
Loginon the preview page. - Gateway returns to
/preview/with a one-time code (gateway_codeby default). - Preview exchanges that code at
/v1/auth/exchangeand auto-fillsAuth Token.- Preview requests Hasura claims (
request_hasura_claims: true), and gateway resolves audience server-side.
- Preview requests Hasura claims (
- Click
Apply / Remountto use that token for GraphQL HTTP/WS requests.
If your auth provider returns access_token in URL hash (implicit flow), the preview page will capture that too.
- Module key:
mfe-internal-operations - Renders internal operations views for system status, runtime canaries, auth gateway apps, MFE management, and cloud billing.
- Shows normalized billing data for configured platform cloud services.
- Includes the legacy async GraphQL chat runtime in
src/module.tsfor contract/reference compatibility. - Emits module events
The backend service lives in the sibling Kubernetes workspace:
../k8s-cms/cloud-billing-service../k8s-cms/manifests/74-cloud-billing-service.yaml../k8s-cms/docs/cloud-billing-service-openapi.yaml
The backend exposes REST endpoints such as GET /billing/summary and GET /healthz for service health and internal checks. The MFE runtime consumes the generated Hasura action, normally gravitee_cloud_billing_api_hasurafetchcloudbilling, through /graphql.
Configuration is provided to the Kubernetes service through BILLING_SERVICES_JSON in the cloud-billing-service-config ConfigMap and supports AWS Cost Explorer, Azure Cost Management Query, GCP Cloud Billing export in BigQuery, OCI Usage API, and mock local data.
The service returns a normalized response with totals, providerTotals, services, breakdown, budgetMonthly, forecastCost, and status fields. Keep provider credentials in the backend environment; the browser only receives normalized billing output.
- Host
dist/example-mfe.jsat a URL reachable by your shell runtime. - Create/update a
cms_modulesrecord usingdirectus/cms-module.seed.json. - In a
cms_block_moduleblock, choose module keymfe-internal-operations. - Use
directus/cms-block-module.props.example.jsonas yourprops_jsonbaseline. - Set:
- optional module access controls:
secured(when true, require sign-in before module renders)requiredRole(optional role required to use this MFE; when missing, module is hidden)hideWhenUnauthorized(optional; hide instead of showing denied message for non-role access failures)unauthorizedMessage(optional denied message when not hidden)- when
requiredRoleis set, the module also forwards that role into async request metadata so backend policy can reject unauthorized API requests
- optional
graphql.httpUrlandgraphql.wsUrloverrides (leave unset to use shell runtime defaults) - optional
graphql.authTokenoverride (usually leave unset and let shell runtime auth provide token) graphql.hasuraRoleis deprecated in async mode and ignored by runtime (role comes from token claims)- optional
graphql.tokenExchange.*:enabledrequestedAudience(set this to the Hasura/GraphQL audience expected by auth hook)requestedAudiences(optional array; request multiple audiences in one exchanged token)requestedScope(optional)requestHasuraClaims(optional boolean; for gateway exchange, ask backend to resolve Hasura audience and mint Hasura-claims token)exchangeUrl(recommended; dedicated backend token exchange endpoint)appSlug(required for backend exchange mode)tokenUrl(optional; defaults from shell auth runtime)clientId(optional; defaults from shell auth runtime)
graphql.submitMutationandgraphql.streamSubscription(defaults are preconfigured forpublish_async_request+graphql_client_async_messages)- optional async transport hints:
async.mode(none,graphql-stream,kafka-graphql-bridge,request-response,subscribe,mixed)async.requestChannelasync.responseChannel
- optional module access controls:
async.correlationIdPath- path mappings:
graphql.submitRequestIdPathgraphql.streamTextPathgraphql.streamDonePathgraphql.streamErrorPath
- optional
vmStatstoken exchange for systems action/authenticated routes:vmStats.tokenExchangePath(defaults to/v1/auth/token-exchange)vmStats.tokenExchangeUrl(absolute override URL for token exchange)vmStats.tokenExchangeAppSlug(optional auth-gateway app slug sent to the token-exchange service for Hasura claims; leave empty to use the shell auth app slug)vmStats.tokenExchangeRequestedAudience(optional; leave empty for Hasura claims so auth-gateway resolves the concrete audience)
- optional
cloudBillingconfig for the billing view:cloudBilling.graphqlEndpoint(defaults to the shell Hasura/graphqlendpoint)cloudBilling.hasuraActionName(optional override)cloudBilling.hasuraActionCandidates(defaults to the generated Gravitee cloud billing actions)cloudBilling.baseUrlandcloudBilling.summaryPathare legacy compatibility settings; runtime billing reads use HasuracloudBilling.services(optional service ID allowlist; empty means all backend-configured services)cloudBilling.currencycloudBilling.lookbackDayscloudBilling.requestTimeoutMscloudBilling.allowLocalFallback
- optional
platformManagementconfig for the Organization Management view:platformManagement.graphqlEndpoint(defaults to the shell Hasura/graphqlendpoint)platformManagement.defaultDomainplatformManagement.defaultCloudProviderplatformManagement.defaultDeploymentStrategyplatformManagement.defaultTemplateSourceRepo(optional; leave blank unless there is a real app repo default)platformManagement.defaultTemplateRef(defaults to the prod deploy ref)platformManagement.productionHostnamePatternplatformManagement.previewHostnamePatternplatformManagement.requestTimeoutMs
The Organization Management view uses Hasura actions for both Directus registry writes and deploy execution. Creating an app from the form now saves the platform_apps row and calls the platform deploy action; saving an existing app queues an update. The GitHub repo field must point at the existing app repository to deploy, not a starter repository. Use the secondary save-only button when the Directus metadata should change without launching a deployment. Destroy resources before deleting a registry row; row deletion is disabled for apps that are still deployed, deploying, destroying, or failed. Use the Deploy secrets dialog to load and update the Terraform Cloud, Cloudflare, and source clone tokens in Vault; auth-gateway admin access uses the existing internal Vault secret. While operations are active, the view polls platform_app_operations and platform_app_operation_steps to show live step status messages.
async.requestChannel, async.responseChannel, and async.correlationIdPath are consumed by this example MFE and injected into submit/stream template variables and emitted events.
publish.yml now publishes module artifacts directly to the module registry service POST /v1/modules/publish using multipart upload.
- Add repository variable(s) or secret(s):
MODULE_REGISTRY_SERVICE_URL_PREVIEWMODULE_REGISTRY_SERVICE_URL_PROD- optional legacy fallback:
MODULE_REGISTRY_SERVICE_URL
- Add repository secrets for module registry publish API auth:
MODULE_REGISTRY_SERVICE_GOOGLE_SERVICE_ACCOUNT_EMAILMODULE_REGISTRY_SERVICE_GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEYMODULE_REGISTRY_SERVICE_GOOGLE_TOKEN_AUDIENCE
- Optional repository variable:
MODULE_REGISTRY_SERVICE_PUBLISH_PATH(default/v1/modules/publish)
- Publish tag
v*(or run Publish workflow manually). - Workflow will:
- build artifacts
- run
npm run publish:registryto generatedist/module.publish.json - run
npm run notify:catalogto uploaddist/example-mfe.jsanddist/module.publish.jsonto the registry service - attach release artifacts in GitHub
publish:registry writes metadata in dist/module.publish.json:
module_versionpublished_atrelease.tagrelease.shabundle.sha256- full
definition+seeddocuments
Module registry service endpoint called by workflow:
POST <resolved-service-url><MODULE_REGISTRY_SERVICE_PUBLISH_PATH>- Default path:
/v1/modules/publish - Channel defaults:
- all publishes use
preview(tag and manual)
- all publishes use
- Service URL selection:
registry_target=preview(default): targetsMODULE_REGISTRY_SERVICE_URL_PREVIEW(fallbackMODULE_REGISTRY_SERVICE_URL)registry_target=production: targetsMODULE_REGISTRY_SERVICE_URL_PROD(fallbackMODULE_REGISTRY_SERVICE_URL)
This repo provides the MFE contract + bundle. Your shell runtime must include or load this module definition at runtime.
If your shell currently only mounts modules from an internal registry, wire this module using the exported host adapter:
- export:
createCmsModuleDefinition()
The bundle also self-registers at:
globalThis.SuncoastMfeRegistry["mfe-internal-operations"]
npm run clean- removedistnpm run clean:dev- removedev-distnpm run typecheck- TS type checknpm run build- compile single JS + copy module definitionnpm run dev- local preview server with live rebuild + preview harnessnpm run publish:registry- generate local publish manifest from built artifactsnpm run notify:catalog- upload built bundle + publish manifest to module registry API
Workflows included:
.github/workflows/ci.yml- runs typecheck/build on push + PR
- uploads dist artifacts
.github/workflows/publish.yml- runs on
v*tags or manual dispatch - manual inputs:
build_mode(production/local)registry_target(preview/production, defaultpreview)
- builds bundle + publish metadata
- uploads artifacts
- creates a GitHub Release for tag pushes
- runs on
Secrets expected by publish workflow:
MODULE_REGISTRY_SERVICE_GOOGLE_SERVICE_ACCOUNT_EMAILMODULE_REGISTRY_SERVICE_GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEYMODULE_REGISTRY_SERVICE_GOOGLE_TOKEN_AUDIENCE
Repository variables used by publish workflow:
MODULE_REGISTRY_SERVICE_URL_PREVIEWMODULE_REGISTRY_SERVICE_URL_PRODMODULE_REGISTRY_SERVICE_URL(legacy fallback)MODULE_REGISTRY_SERVICE_PUBLISH_PATH(optional, default/v1/modules/publish)