Skip to main content

App Manifest

Every IrisX App has exactly one iris-app-manifest.ts. It is the app's contract with the platform: it names the app, decides which customers can install it, describes how it appears in the Marketplace, declares the API access it needs, and registers the extensions that make up its functionality. The platform reads this file when your app is submitted, when a customer installs it, and every time it runs.

This page documents every field of the manifest.

Source revision

Written against @trackunit/iris-app-api v2.4.9, manifest specVersion 1.1. The manifest is fully typed, so your editor will autocomplete field names and valid values, and tsc will reject anything invalid. If this page and the type in your node_modules ever disagree, the type wins.

Who sets what

Not every field in the manifest is yours to write. The SDK tooling generates some of them during the build and overwrites whatever it finds, so editing those by hand is wasted effort at best and a confusing build failure at worst.

You set: moduleFormat, installation, marketplace, scopes, tokenCallback, cspHeader, permissionsPolicy, extensions, customFieldDefinitions.

The tooling sets: specVersion, sdkVersion, moduleFederationName, dependencies, devDependencies. These appear in your manifest file, and you should read them, but leave them alone.

Field index

FieldRequiredWho sets it
specVersionYesTooling
moduleFederationNameYesTooling
moduleFormatNoYou
sdkVersionNoTooling
installationYesYou
marketplaceYesYou
scopesNoYou
tokenCallbackNoYou
cspHeaderNoYou
permissionsPolicyNoYou
dependenciesYesTooling
devDependenciesYesTooling
extensionsYesYou
customFieldDefinitionsNoYou

specVersion

The version of the manifest schema itself, currently the literal string "1.1". It tells the platform which manifest features your app declares, which is how older and newer apps can coexist on the same platform.

The SDK tooling sets this. Do not change it by hand — bumping it does not opt you into anything, it just makes your manifest claim a shape it does not have.

moduleFederationName

The unique identifier the platform uses to load your app at runtime via Module Federation. It is derived automatically from your workspace name and your app's name, both read from your app's package.json.

Three constraints matter:

  • It must be unique across every published IrisX App, not just within your workspace.
  • It may only contain alphanumeric characters and underscores.
  • It cannot be changed after your app has been published for the first time. Renaming it later breaks the platform's ability to find your app.

Because it derives from your app name, the practical consequence is that renaming a published app's directory or package is not a safe operation. Pick a name you can live with before you publish.

moduleFormat

Which module format your app is built with, either "esm" or "cjs". This is the one build-related field you may set yourself.

Optional. When omitted the platform assumes "cjs".

sdkVersion

The resolved @trackunit/iris-app version that was installed when your app was built. The Vite build executor stamps it onto the emitted manifest during the build.

You do not set this. The host uses it to decide whether your app needs a transitional layout compatibility behaviour, so an inaccurate value produces subtly wrong rendering rather than a clean error.

installation

Who can install your app, and how. This is the field that decides whether a given customer ever sees your app at all, so it is worth getting right before you submit.

installation comes in three shapes and you pick exactly one. All three require pricingPlanPolicy.

Targeting specific accounts

Use this when you know which accounts should have access, or when the app is for everyone.

installation: {
pricingPlanPolicy: { ALL_PLANS: "ON_DEMAND" },
accountIds: "ALL_ACCOUNTS",
}

accountIds is either the string "ALL_ACCOUNTS" or a non-empty array of account IDs.

Targeting everyone except some accounts

Use this to open the app up broadly while excluding specific accounts. accountIds must be "ALL_ACCOUNTS" in this shape — excluding accounts from an explicit allow-list makes no sense, and the type will not let you.

installation: {
pricingPlanPolicy: { ALL_PLANS: "ON_DEMAND" },
accountIds: "ALL_ACCOUNTS",
excludeAccountIds: ["account-id-to-exclude"],
}

Targeting whole organizations

Use this to grant access by organization rather than account. This shape uses allowOrganizationIds instead of accountIds — the two are mutually exclusive, so set one or the other, never both.

installation: {
pricingPlanPolicy: { ALL_PLANS: "ON_DEMAND" },
allowOrganizationIds: ["organization-id"],
}

pricingPlanPolicy

How the app installs, per pricing plan. Either a map from subscription package to policy, or the single-key ALL_PLANS form. You cannot combine them.

// Per plan
pricingPlanPolicy: { EXPLORE: "ON_DEMAND", EVOLVE: "PREINSTALLED" }

// Every plan
pricingPlanPolicy: { ALL_PLANS: "ON_DEMAND" }

The subscription packages are VIEW, COLLECT, INSIGHT, ENTRY, EXPLORE, EVOLVE, EXPAND, LINK, LIFT, LEAP and CUSTOMER_PORTAL.

A plan you leave out of the map is a plan your app is not available on. This is the most common way to accidentally hide an app from customers who should have it, because nothing fails — the app simply never appears.

The five policies:

PolicyBehaviour
PERMANENTAlways installed, and the customer cannot remove it.
PREINSTALLEDInstalled to begin with, and the customer can remove it.
ON_DEMANDThe customer installs and removes it themselves.
ON_DEMAND_REQUIRES_APPROVALThe Trackunit billing team must approve before the customer can install. Behaves as ON_DEMAND afterwards.
PREINSTALLED_REQUIRES_APPROVALThe Trackunit billing team must approve, then it installs automatically. Behaves as PREINSTALLED afterwards.

irisAppsInsideBundle

Optional. The identifiers of other IrisX Apps packaged inside this one, when you ship several apps as a bundle. Leave it out unless you are bundling.

marketplace

How your app presents itself in the Trackunit Marketplace. Everything a prospective customer sees before installing comes from here.

marketplace: {
showInMarketplace: true,
name: "Weather Availability",
description: "Shows whether an asset is safe to operate in current conditions.",
fullDescriptionPath: "description.md",
logoPath: "logo.png",
categories: ["EFFECTIVE_OPERATIONS"],
tags: ["weather", "safety"],
supportUrl: "https://example.com/support",
}
PropertyRequiredPurpose
showInMarketplaceYesWhether the app is listed at all.
nameYesThe app's display name.
descriptionYesShort summary for the Marketplace card. Plain text only — no markdown.
fullDescriptionPathYesPath to a markdown file with the long description.
logoPathNoPath to the logo image.
tagsNoFree-text tags to make the app easier to find.
categoriesNoMarketplace categories. See the list below.
assetsNoScreenshots and videos. See below.
homePageUrlNoHomepage of the company building the app.
supportUrlNoDirect support page for this app.
blueprintUrlNoLink to the app's blueprint, if it has one.

fullDescriptionPath and logoPath are resolved relative to your app's assets/ directory. For a file at apps/my-app/assets/description.md, the value is just description.md.

categories

One or more of: AI_AND_AGENTS, ASSET_MANAGEMENT, BLUEPRINTS, COMPLIANCE, CONSULTANTS, DATA_FEEDS, DOCUMENTATION, EFFECTIVE_OPERATIONS, EQUIPMENT_PROTECTION, OEM_PARTNER_SOLUTIONS, OPTIMIZE_BUSINESS, RENTAL_MANAGEMENT, SERVICE_AND_MAINTENANCE, SUSTAINABILITY.

assets

Images and videos for the Marketplace listing. Images reference a file in your assets/ directory; videos reference a YouTube URL.

assets: [
{ type: "IMAGE", path: "screenshot-fleet.png" },
{ type: "VIDEO", url: "https://www.youtube.com/watch?v=..." },
]

There is also a fullDescriptionImages property on this object. The build plugins populate it from the images your full description references, and the platform uses it to decide which asset files may be served publicly. Do not set it yourself.

Availability per plan moved

If you are used to controlling which subscription packages an app is compatible with through a marketplace property, that is no longer where it lives. At specVersion 1.1 plan availability is expressed entirely through installation.pricingPlanPolicy.

scopes

The API access your app needs. When Trackunit Manager launches your app it hands it a token, and that token is deliberately not a general-purpose credential — it carries only the scopes your manifest declares. Anything you did not ask for, you cannot do.

Each entry pairs a scope with whether it is optional. A single entry may be written as an object rather than an array.

scopes: [
{ scope: "asset.view" },
{ scope: "account.asset.manage", optional: true },
]

Required and optional scopes

Leave optional off, or set it to false, and the scope is required. Set it to true and the customer installing your app can refuse that particular scope while still installing the app.

The two cases behave very differently at runtime, and the difference matters when you decide which scopes to mark optional:

  • A denied required scope stops the app. The customer sees a consent prompt instead of your app, and it stays that way until they grant it.
  • A denied optional scope is silent. Your app launches normally and receives a token that simply lacks that scope. Nothing tells your code which scopes were dropped, so calls needing the denied scope fail at the point of use.

There is no SDK method that reports your granted scopes. If your app needs to adapt its UI to a denied optional scope, the only way to detect it is to decode the token yourself and inspect its scp claim. Design for that before marking a scope optional — an optional scope you cannot detect is an optional scope whose absence turns into a confusing runtime error for the customer.

Omitting scopes

scopes is optional. An app that never calls a Trackunit API can leave it out, and the platform applies a no-api-access marker instead of any real scope. That is what makes omission safe rather than accidentally over-permissive.

Finding the scope a call needs

Rather than guessing from the table below, work backwards from the operation you want to perform. Every field in the public GraphQL schema that requires authorization documents its own requirement: the schema's @authorization directive surfaces in each field's description as a Required scopes: line.

A field listing several scopes needs at least one of them, not all of them.

Two ways to read it:

So when a query comes back with an authorization error, look up that field in the schema, read its Required scopes: line, and add one of the listed values to your manifest.

Two related points. Preview fields need both an appropriate scope and the TU-PREVIEW header — see calling the GraphQL API. And because the manifest is typed, your editor autocompletes valid scope values as you type them and tsc rejects anything misspelled, so you should never be guessing at the string itself.

Validation when you submit

Scope values are checked twice, against two different sources.

TypeScript checks the value against the SDK's scope type when you build. The platform checks it again against the authorization server when you submit your app. These are separate lists, so a manifest that type-checks cleanly can still be rejected at submission with an error naming the offending scope. If that happens, the scope you asked for is not one the authorization server recognises for apps, and no amount of rebuilding will change that — pick a different scope or talk to your Trackunit representative.

Available scopes

ScopeDescription
account.access-management.manageCan manage Access Management
account.alert.manageCan manage alerts
account.alert.viewCan view alerts
account.api-key.manageCan manage API keys
account.asset.manageCan manage assets
account.audit-log.viewCan view audit logs
account.billing.manageCan manage billing
account.branding.manageCan manage Account Branding
account.classic-manager.accessCan access Classic Manager
account.custom-fields.manageCan manage custom fields
account.customer.manageCan manage customers
account.deleteCan delete account
account.equipment-pooling.manageCan manage Equipment Pooling
account.equipment-pooling.viewCan view Equipment Pooling
account.event.manageCan manage events
account.event.report-issueCan report issue as event
account.group.manageCan manage groups
account.internal-tools.brandingCan manage branding in internal tools
account.internal-tools.package-overviewCan view package overview in internal tools
account.iris.app.developerCan develop apps
account.iris.app.store-secretCan store app secrets
account.irisx.analytics.adminCan administer IrisX analytics
account.irisx.analytics.read_onlyCan view IrisX analytics
account.irisx.manageCan manage IrisX configuration
account.license.viewCan view licenses
account.manageCan manage account
account.marketplace.app.approveCan approve apps in marketplace
account.marketplace.app.installCan install apps from marketplace
account.my-home.canvas.editCan edit My Home canvases
account.my-home.manageCan manage My Home
account.organization.manageCan manage organizations
account.partner.manageCan manage partners
account.remote-commands.configuration-tool.viewCan view the remote command configuration tool
account.rental.viewCan view rental data
account.service-management.network.manageCan manage networks in service management
account.service-management.network.viewCan view networks in service management
account.service-management.service-plan.assignment.manageCan manage assignment of service plans in service management
account.service-management.service-plan.assignment.reportCan report assignment of service plans in service management
account.service-management.service-plan.assignment.service-registration.registerCan manage service registration on service plans in service management
account.service-management.service-plan.assignment.service-registration.viewCan view service registration on service plans in service management
account.service-management.service-plan.assignment.viewCan view service plan assignments in service management
account.service-management.service-plan.manageCan manage service plans in service management
account.service-management.service-plan.viewCan view service plans in service management
account.show-historical-dataCan view historical data
account.site-emissions.asset-history.viewCan view historical emissions data for assets not currently on site
account.site.manageCan manage sites
account.subscription.package-manageCan manage subscription packages
account.support-accessCan use impersonation feature
account.support-access.approval-flowCan approve support access requests
account.support-access.extended-periodCan use extended support access period
account.support-access.pre-approvedCan use pre-approved support access
account.top-level.createCan create top-level accounts
account.user.manageCan manage users
account.viewCan view account
asset.viewCan view assets
site.viewCan view sites

tokenCallback

Where the platform should call to hand your app credentials after a customer installs it. Use this when your app needs to call Iris APIs from your own backend rather than from the browser.

tokenCallback: {
url: "https://example.com/trackunit/token",
}

Setting tokenCallback makes scopes mandatory — a callback that receives a token carrying no scopes would be pointless, and submission is rejected if you declare one without the other.

For how to receive and use the token on your side, see app tokens for asynchronous work.

cspHeader

The Content Security Policy for your app's iframe. If your app talks to anything outside Trackunit, this is the field that decides whether the browser lets the request through.

Undeclared domains fail silently

Every external origin your app contacts must be listed here. If it is not, the browser blocks the request in the client — there is no server-side error to find in a log, and the failure often looks like an unrelated bug in your own code. The list is also audited during app approval, so an app calling undeclared domains will not pass review.

Every directive is optional; set only the ones you need.

cspHeader: {
"connect-src": ["'self'", "https://api.example.com"],
"img-src": ["'self'", "https://cdn.example.com"],
}

Supported directives

child-src, connect-src, font-src, form-action, frame-src, img-src, media-src, script-src, script-src-attr, script-src-elem, style-src, style-src-attr, style-src-elem, worker-src, plus sandbox, upgrade-insecure-requests and webrtc.

Most directives take an array of sources. You can use ordinary origins, the standard CSP keywords 'none', 'self', 'unsafe-inline', 'unsafe-eval', 'unsafe-hashes', 'strict-dynamic' and 'report-sample', and nonces (nonce-...) or hashes (sha256-..., sha384-..., sha512-...) where the directive allows them. Not every keyword is valid on every directive, and the type enforces that.

Two directives are not arrays: upgrade-insecure-requests takes a boolean, and webrtc takes 'allow' or 'block'.

For background on what the directives actually mean, see the MDN CSP documentation.

sandbox

sandbox controls the iframe sandbox around your app, and behaves differently from the other directives: the values you list are merged with the platform defaults, not substituted for them. The defaults are allow-scripts, allow-downloads, allow-same-origin, allow-forms and allow-popups, so you use this property to request extra capabilities rather than to restrict existing ones.

cspHeader: {
sandbox: ["allow-modals"],
}

The accepted values are allow-downloads, allow-downloads-without-user-activation, allow-forms, allow-modals, allow-orientation-lock, allow-pointer-lock, allow-popups, allow-popups-to-escape-sandbox, allow-presentation, allow-same-origin, allow-scripts, allow-storage-access-by-user-activation, allow-top-navigation and allow-top-navigation-by-user-activation.

permissionsPolicy

Access to browser features and shared machine resources inside your app's iframe — the clipboard, geolocation, camera, fullscreen and so on. Without an entry here the iframe is not permitted to use the feature, regardless of whether the user would have granted it.

permissionsPolicy: {
"clipboard-write": ["self"],
geolocation: ["self"],
}

Each directive takes either "*" or an array of "src", "self", and explicit http:// or https:// origins.

The available directives cover the full Permissions Policy surface, from accelerometer through xr-spatial-tracking, including autoplay, bluetooth, camera, clipboard-read, clipboard-write, display-capture, encrypted-media, fullscreen, geolocation, microphone, midi, payment, picture-in-picture, screen-wake-lock, serial, usb and web-share. Your editor will list them all.

Experimental specification

The Permissions Policy specification is still experimental and browser support varies by directive. Test the features you depend on in the browsers your customers actually use, and do not assume a directive is honoured everywhere. See the MDN Permissions-Policy documentation.

dependencies

The full map of your app's runtime dependencies, parsed automatically from your workspace's root package.json.

The platform uses this list for performance optimisation and, more importantly, for security scanning during the approval process. A vulnerable transitive dependency is something a reviewer can and will raise, so it is worth keeping your dependency tree lean for reasons beyond bundle size.

devDependencies

The same, for development dependencies. Also parsed automatically and also security-scanned.

extensions

The array of extensions bundled into this app. An app is a container; extensions are where your actual functionality lives, each one targeting a specific extension point in Trackunit Manager.

The NX generators add an entry here automatically when you create a new extension, so most of the time you will not touch this field. The exception is sharing: if one extension belongs in several apps, you must add it to each app's array yourself.

Each entry is an import of that extension's own extension-manifest.ts. The available extension types and their individual configuration are documented alongside the extension points.

customFieldDefinitions

The custom field definitions your app owns. Custom fields let an app attach its own data attributes to Trackunit entities such as assets, accounts, groups and sites, and the fields your app defines belong to your app.

Optional. Omit it entirely if your app stores no data of its own. See custom fields for the available field types, their configuration, and how to read and write values from your extension code.