Manifest and lifecycle
A Corvina application is a software that is not part of Corvina codebase but that can show something inside Corvina as if it were part of our platform. Every web application can be a Corvina application!
You can think an external application as a micro-frontend of Corvina that follows the backend for frontend pattern (more details about these definitions here, the Martin Fowler website).
We compose our micro-fronted using iframes in that way:
We'll cover the technical aspect in the next sections.
A Corvina application can be added to the Corvina app store and then installed by a Corvina user in its own organization.
You can host your application in any private or public cloud solution. Your application === your hosting.
Manifest.json
Each Corvina application is described by a manifest.json file that contains some information useful for the integration.
{
// unique identifier of the application
"key": "corvina-app-nodered",
// name of the application in the app store
"name": {
"value": "NodeRED Cloud Service",
"i18n": "name" // optional, if not present the value is used
},
// description of the application in the app store
"description": {
"value": "Create NodeRED instances managed by Corvina in the cloud",
"i18n": "description" // optional, if not present the value is used
},
// type of the application, can be APP or WIDGET. Last one does not accept every manifest property as APP.
// Apps with WIDGET type are used to unlock the widgets in dashboards.
"type": "APP",
// status of the application in the app store, can be ACTIVE or UNDER_EVALUATION.
// Apps with UNDER_EVALUATION status cannot be installed, those apps are used for preview purpose.
"status": "ACTIVE",
// images of the application in the app store carousel
"images": [
{
"url": "/static/flow-1.webp",
"thumbnailUrl": "/static/flow-1.thumbnail.webp"
},
{
"url": "/static/flow-2.png",
"thumbnailUrl": "/static/flow-2.thumbnail.png"
}
],
// image of the application in the app store list
"coverImageUrl": "/static/cover.png",
// url of the application in all the following relative urls
"baseUrl": "https://nodered.corvina.cloud/v1",
// If true, we enable the device to call our services calling key.device.corvina.io
// (or key.device.corvina.cloud) using its own certificate
"enableDeviceAccess": false,
// version of the manifest.json file, we use it to check if the application has been updated.
// You must use semantic versioning (https://semver.org/)
"apiVersion": "1.0.21",
// authentication type of the application
"authentication": {
// all our application tokens are JWT tokens (see https://jwt.io/)
"type": "JWT"
},
// vendor of the application, used in the app store details page
"vendor": {
"name": "Corvina",
"website": "https://corvina.io/",
// email of the vendor, used in the app store details page (optional).
// This email can be used by Corvina users to contact you for support or further information.
// This email can also be used by Corvina to send you notifications about API deprecations, security issues, scheduled maintenance, etc.
"email": "info@corvina.io"
},
"links": {
// tell to Corvina where to find the manifest.json file (if not provided, Corvina will concatenate the baseUrl with /manifest.json).
// We will poll on this url to check if the application has been updated.
"self": "/manifest.json",
// You can provide a changelog reference that will be shown in the app store details page (optional)
"changelog": "/changelog.html",
},
"lifecycle": {
// Corvina will call this url when the application is installed
"installed": "/installed",
// Corvina will call this url when the application is uninstalled
"uninstalled": "/uninstalled",
// Corvina will call this url when the application is upgraded (optional)
"upgradeOk": "/upgradeOk",
// Corvina will call this url when the application upgrade fails (optional)
"upgradeKo": "/upgradeKo",
// Corvina will call this url when the active payment plan is renewed (optional if app is free)
"renew": "/renew"
},
"hooks": {
// information used after installation to create a menu entry in the Corvina app
"globalPage": {
// unique identifier of the menu entry
"id": "corvina-app-nodered-globalPage",
// name of the menu entry
"title": {
"value": "NodeRED Cloud Service",
"i18n": "globalPage.title" // optional, if not present the value is used
},
// url of the menu entry, it's the first url called when the user click on the menu entry and we use it to load the iframe
"url": "https://nodered.corvina.cloud/#/",
// svg icon of the menu entry
"iconUrl": "/static/icon.svg",
// default false, if true Corvina will not add query params to the url
"avoidCorvinaQueryParams": true
},
// each item in the array will create a menu entry in the Corvina app navigation drawer
"navigationDrawerPages": [
{
// name of the menu entry
"title": {
"value": "NodeRED Instances",
"i18n": "navigationDrawerPage.instances.title" // optional, if not present the value is used
},
// url of the menu entry, it's the first url called when the user click on the menu entry and we use it to load the iframe
"url": "https://nodered.corvina.cloud/#/instances",
// svg icon of the menu entry
"iconUrl": "/static/icon.svg",
// default false, if true Corvina will not add query params to the url
"avoidCorvinaQueryParams": true
},
{
// name of the menu entry
"title": {
"value": "NodeRED Instances/Sub item", // the slash creates nested menù items
"i18n": "navigationDrawerPage.instances.subItem.title" // optional, if not present the value is used
},
// url of the menu entry, it's the first url called when the user click on the menu entry and we use it to load the iframe
"url": "https://nodered.corvina.cloud/#/overview",
// svg icon of the menu entry
"iconUrl": "/static/icon.svg",
// default false, if true Corvina will not add query params to the url
"avoidCorvinaQueryParams": true
}
]
},
// some value identified by i18n keys need to be translated. This section declare where the translation files are located.
"translations": {
// the key is the language code underscore the region code, the value is the url of the translation file.
// language code follows the ISO 639-1 standard, region code follows the ISO 3166-1 alpha-2 standard.
"urls": {
"en_US": "/i18n/en_US.json",
"it_IT": "/i18n/it_IT.json"
}
},
// scopes of the application, used to define the permissions of the application in terms of application role or device role.
// Corvina will creates a service account for the application and will assign the scopes to the service account.
"scopes": {
"applications": [
"iam.devices.read",
"iam.models.read"
],
// Corvina frontend exchanges a special token with no user permissions in it, unless this
// attribute is set to true. Doing so results in a token with common permissions between those
// defined in scopes.applications and the user permissions set in the platform
"userImpersonation": false,
"devices": [
{
"deviceGroups": [
"*"
],
"generalPermission": "REGULAR_USER",
"modelPermissions": [
"**"
]
}
]
},
// In case your application needs other Corvina applications to works, you have to list needed application keys (optional)
"dependsOn": ["corvina-app-artifact-registry"],
// additional roles to add at installation time
"additionalRoles": [
{
"name": "readonly",
"description": "Readonly role for Nodered app",
"linkedRoles": ["app_role-corvina-app-y"] // The additional app role can be linked to these app roles, inheriting permissions. Useful for device access permission management on dependent apps.
}
],
// The app role can be linked to these app roles, inheriting permissions. Useful for device access permission management on dependent apps.
"linkedRoles": ["app_role-corvina-app-x"],
// Does the usage of this app require a payment? If so, set free to false. Default true.
"free": false,
// Does the app request in-app credits transactions? If so, set the attribute to true. Default false.
"inAppPurchases": true,
// a paid app must expose at least a payment plan. A plan can be one-time only or recurrent,
// and can have a free trial period beforehand
"paymentPlans": [
{
"id": "planId1", // unique id
"label": {
"value": "My cute plan",
"i18n": "plan.id.label" // optional, if not present the value is used
},
"description": {
"value": "this is the description",
"i18n": "plan.id.description" // optional, if not present the value is used
},
// this amount covers installation fees and the first recurrent period, if any
"amount": 15000, // unit is one thousandth of this value, so Corvina shows 15 credits
"recurrent": { // optional
"period": "P1Y", // ISO-8601 period formats PnYnMnD
"amount": 20000 // unit is one thousandth of this value, so Corvina shows 20 credits
},
"trial": { // optional
"period": "P7D" // ISO-8601 period formats PnYnMnD
},
// arbitrary options, transparent to Corvina but forwarded to the app and shown on the platform
"options": [
{
"key": "opt1",
"msg": { // shown in the plan detail
"value": "This plan includes 3 NodeRed instances",
"i18n": "opt.key1" // optional, if not present the value is used
},
"val": <any>
}
],
// a plan cannot be removed, it should be deprecated
"deprecated": false,
// Used to determine the plans hierarchy in ordering and during a plan upgrade.
// A lower level plan can be changed only at expiration of the previous active one.
"level": 0
},
]
}
Each relative url in the manifest.json file is relative to the baseUrl
property, but you can use absolute urls. For example, if the baseUrl
is https://nodered.corvina.cloud/v1
and the lifecycle.installed
is /installed
, the url called by Corvina will be https://nodered.corvina.cloud/v1/installed
. If the lifecycle.installed
is https://nodered.corvina.cloud/v2/anotherInstallation
, the url called by Corvina will be https://nodered.corvina.cloud/v2/anotherInstallation
regardless of the baseUrl
property.
All the urls in the manifest.json
file are called by Corvina, so they must be accessible from the internet. You have to take care of the content of the response of each url.
In you use dependsOn
field, you have to install needed listed applications, before to install yours. On frontend side the installation button is disabled if needed listed applications are not installed yet.
It's very important to understand that Corvina will create a service account for the application and will assign the scopes to the service account. The service account, is identified by a couple of strings:
- client id
- client secret
that are used to authenticate the application to Corvina. Corvina will pass those information to the external app during the installation API call.
Installation
When a user installs an application, Corvina will call the installation API of the application. The installation API is defined in the manifest.json file, Corvina will perform a POST request to the url defined in the lifecycle.installed
property. The payload is a json object:
{
// unique identifier of the application, as defined in the manifest.json file
"key": "corvina-app-nodered",
// version of the manifest.json file
"apiVersion": "1.0.21",
// client id of the service account created for the application
"clientId": "user-service-noderedadmin@myorg",
// client secret of the service account created for the application
"clientSecret": "43024c91-cd1e-4877-bc21-6e61b2d80a49",
// identifier of the organization where the application is installed
"organizationId": 36,
// identifier of the Corvina instance
"instanceId": "c9beedd2-6c15-4c85-bb2f-50ec579c9ae6",
// the string version of the organization identifier
"orgResourceId": "exor.example",
// an event type that can be used to distinguish why this API is called
"eventType": "installed",
// the realm name of the organization where the application is installed
"realm": "myorg",
// only the user with this role can access the application, you can check if the jwt received
// in the request contains this role. We'll explain this better in the frontend integration section
"realmValidationRole": "monitoring.roles.app_nodered_administrator",
// the platform host of the Corvina instance where the application is installed
"baseUrl": "https://app.corvina.io",
// the websocket url of the Corvina instance where the application is installed
"wsBaseUrl": "wss://app.corvina.io",
// the API url of the Corvina instance where the application is installed
"apiBaseUrl": "https://app.corvina.io",
// the hostname of the Corvina organization
"hostname": "myhostname",
// the host of our authentication server
"authBaseUrl": "https://auth.corvina.io",
// this is the OpenId connect discovery endpoint, you should use it to retrieve the public key
// of the jwt token in order to verify the Corvina JWT signature.
"openIdConfigurationUrl": "https://auth.corvina.io/auth/realms/sample1/.well-known/openid-configuration",
// In case of dependsOn in manifest.json, you will receive dependencies' manifests to get needed information.
// In dependencies you receive a JSON that has application key as key and application manifest as value
"dependencies": {
"corvina-app-artifact-registry": {
"key": "corvina-app-artifact-registry",
"name": ...
}
},
// if the app is not free, the chosen plan id by the user is sent...
"planId": "planId1",
// ...along with the date to expect a renewal. App should block the usage if this date is expired
"endDate": "2025-04-10T00:00:00Z",
// whether the current installation is considered free trial. App could apply in-app labels limited to this period
"freeTrial": true
}
This is a webhook, so we follow our Webhook guidelines.
If the installation succeeds, the user will see that the application is in status INSTALLED and will be able to access it. If the installation does not succeed, the user will see that the application is not installed and will be able to retry the installation.
The following is an example of a successful installation:
Uninstallation
When a user uninstalls an application, Corvina will call the uninstallation API of the application. The uninstallation API is defined in the manifest.json
file, Corvina will performa a POST request to the url defined in the lifecycle.uninstalled
property. The payload is a json object:
{
// unique identifier of the application, as defined in the manifest.json file
"key": "corvina-app-nodered",
// version of the manifest.json file
"apiVersion": "1.0.21",
// identifier of the organization where the application is installed
"organizationId": 36,
// identifier of the Corvina instance
"instanceId": "c9beedd2-6c15-4c85-bb2f-50ec579c9ae6",
// an event type that can be used to distinguish why this API is called
"eventType": "uninstalled",
// the platform host of the Corvina instance where the application is installed
"baseUrl": "https://app.corvina.io",
// the API url of the Corvina instance where the application is installed
"apiBaseUrl": "https://app.corvina.io",
// the host of our authentication server
"authBaseUrl": "https://auth.corvina.io",
// this is the OpenId connect discovery endpoint, you can use it to retrieve the public key of the jwt token in order to verify the Corvina JWT signature.
"openIdConfigurationUrl": "https://auth.corvina.io/auth/realms/sample1/.well-known/openid-configuration"
}
This is a webhook, so we follow our Webhook guidelines.
In this example, the application is uninstalled from the organization with id 36 and instance id c9beedd2-6c15-4c85-bb2f-50ec579c9ae6
If you create some resources in the installation API, you can delete them in the uninstallation API
If the uninstallation does not succeed, the user will see that the application is in status "UNINSTALLATION_FAILED" and will be able to retry the uninstallation indefinitely.
Renew
Corvina provides the management of the lifecycle of an application, including renewing it. This process covers moving from a free trial to a recurrent renewals or a one-time payment and renewing recurrent periods.
The renew endpoint is defined in the manifest.json
file, and Corvina will perform a POST request to the URL specified in the lifecycle.renew
property. This request should be handled idempotently, as Corvina will provide the updated expiration date for the application.
The payload is a json object:
{
"key": "corvina-app-nodered",
"apiVersion": "1.0.21",
"baseUrl": "https://app.corvina.io",
"apiBaseUrl": "https://app.corvina.io",
"authBaseUrl": "https://auth.corvina.io",
"openIdConfigurationUrl": "https://auth.corvina.io/auth/realms/sample1/.well-known/openid-configuration",
"instanceId": "c9beedd2-6c15-4c85-bb2f-50ec579c9ae6",
"organizationId": 36,
"eventType": "renew",
// new app expiration
"endDate": "2026-10-10T00:00:00Z",
// active plan id
"planId": "1month",
// whether the plan is in free trial or not, used in plan upgrade
"freeTrial": "false"
}
This is a webhook, so we follow our Webhook guidelines.
In this example, the plan is renewed for 18 months after the free trial period expiration, visibile in the installation object.
Translations
We want to make our app available to users in languages other than English, so the strings in the manifest.json
file are translatable.
A common structure for translatable string is:
{
"name": {
"value": "My name",
"i18n": "my-name"
},
}
The value
field is the default value of the string, and the i18n
field is the key of the string in the translation file. The translation files are listed in the translations field of the manifest.json file.
Each translation file is a one-level json file with the following structure:
{
"description": "My localized description",
"key1": "translation of key1",
"key2": "translation of key2",
"my-name": "My name in the current language",
"plan.id.label": "My cute plan"
}
Limitations:
- the maximum size of a translation file is 32 kB
- a translation string cannot be longer than 1kB
The translation files are loaded at runtime from Corvina without any authentication (they must be publicly available).