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.
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
| Field | Required | Who sets it |
|---|---|---|
specVersion | Yes | Tooling |
moduleFederationName | Yes | Tooling |
moduleFormat | No | You |
sdkVersion | No | Tooling |
installation | Yes | You |
marketplace | Yes | You |
scopes | No | You |
tokenCallback | No | You |
cspHeader | No | You |
permissionsPolicy | No | You |
dependencies | Yes | Tooling |
devDependencies | Yes | Tooling |
extensions | Yes | You |
customFieldDefinitions | No | You |
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:
| Policy | Behaviour |
|---|---|
PERMANENT | Always installed, and the customer cannot remove it. |
PREINSTALLED | Installed to begin with, and the customer can remove it. |
ON_DEMAND | The customer installs and removes it themselves. |
ON_DEMAND_REQUIRES_APPROVAL | The Trackunit billing team must approve before the customer can install. Behaves as ON_DEMAND afterwards. |
PREINSTALLED_REQUIRES_APPROVAL | The 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",
}
| Property | Required | Purpose |
|---|---|---|
showInMarketplace | Yes | Whether the app is listed at all. |
name | Yes | The app's display name. |
description | Yes | Short summary for the Marketplace card. Plain text only — no markdown. |
fullDescriptionPath | Yes | Path to a markdown file with the long description. |
logoPath | No | Path to the logo image. |
tags | No | Free-text tags to make the app easier to find. |
categories | No | Marketplace categories. See the list below. |
assets | No | Screenshots and videos. See below. |
homePageUrl | No | Homepage of the company building the app. |
supportUrl | No | Direct support page for this app. |
blueprintUrl | No | Link 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.
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:
- Browse interactively in the GraphQL Explorer.
- Download the public schema and search 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
| Scope | Description |
|---|---|
account.access-management.manage | Can manage Access Management |
account.alert.manage | Can manage alerts |
account.alert.view | Can view alerts |
account.api-key.manage | Can manage API keys |
account.asset.manage | Can manage assets |
account.audit-log.view | Can view audit logs |
account.billing.manage | Can manage billing |
account.branding.manage | Can manage Account Branding |
account.classic-manager.access | Can access Classic Manager |
account.custom-fields.manage | Can manage custom fields |
account.customer.manage | Can manage customers |
account.delete | Can delete account |
account.equipment-pooling.manage | Can manage Equipment Pooling |
account.equipment-pooling.view | Can view Equipment Pooling |
account.event.manage | Can manage events |
account.event.report-issue | Can report issue as event |
account.group.manage | Can manage groups |
account.internal-tools.branding | Can manage branding in internal tools |
account.internal-tools.package-overview | Can view package overview in internal tools |
account.iris.app.developer | Can develop apps |
account.iris.app.store-secret | Can store app secrets |
account.irisx.analytics.admin | Can administer IrisX analytics |
account.irisx.analytics.read_only | Can view IrisX analytics |
account.irisx.manage | Can manage IrisX configuration |
account.license.view | Can view licenses |
account.manage | Can manage account |
account.marketplace.app.approve | Can approve apps in marketplace |
account.marketplace.app.install | Can install apps from marketplace |
account.my-home.canvas.edit | Can edit My Home canvases |
account.my-home.manage | Can manage My Home |
account.organization.manage | Can manage organizations |
account.partner.manage | Can manage partners |
account.remote-commands.configuration-tool.view | Can view the remote command configuration tool |
account.rental.view | Can view rental data |
account.service-management.network.manage | Can manage networks in service management |
account.service-management.network.view | Can view networks in service management |
account.service-management.service-plan.assignment.manage | Can manage assignment of service plans in service management |
account.service-management.service-plan.assignment.report | Can report assignment of service plans in service management |
account.service-management.service-plan.assignment.service-registration.register | Can manage service registration on service plans in service management |
account.service-management.service-plan.assignment.service-registration.view | Can view service registration on service plans in service management |
account.service-management.service-plan.assignment.view | Can view service plan assignments in service management |
account.service-management.service-plan.manage | Can manage service plans in service management |
account.service-management.service-plan.view | Can view service plans in service management |
account.show-historical-data | Can view historical data |
account.site-emissions.asset-history.view | Can view historical emissions data for assets not currently on site |
account.site.manage | Can manage sites |
account.subscription.package-manage | Can manage subscription packages |
account.support-access | Can use impersonation feature |
account.support-access.approval-flow | Can approve support access requests |
account.support-access.extended-period | Can use extended support access period |
account.support-access.pre-approved | Can use pre-approved support access |
account.top-level.create | Can create top-level accounts |
account.user.manage | Can manage users |
account.view | Can view account |
asset.view | Can view assets |
site.view | Can 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.
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.
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.