App Lifecycle
This page is for app developers thinking about what happens after the first deploy. A successful app in production needs versioning that doesn't break customer data, an upgrade story that's safe across hundreds of tenants, and event hooks for the moments when something has changed (installed, upgraded, uninstalled).
The basic shape: each app version is deployed once to the platform's app registry and then installed by individual tenants. When you ship a new version, tenants get upgraded according to rules you set in manifest.json. Schema migrations apply automatically per-tenant the next time their data is touched. Lifecycle events fire so other apps (and your own) can react.
The most important practical question is: how do I ship a change without breaking the customer who's already running the previous version in production? The semantic versioning rules below answer most of it; the rest comes down to designing schema changes carefully — additive changes are safe, destructive changes require coordination.
Versioning
Apps use Semantic Versioning (SemVer):
1.0.0
│ │ │
│ │ └─ Patch: Bug fixes, non-breaking changes (1.0.0 → 1.0.1)
│ └─── Minor: New features, backward-compatible (1.0.0 → 1.1.0)
└───── Major: Breaking changes (1.0.0 → 2.0.0)
Version Rules
Which number you bump isn't cosmetic — it tells the platform how aggressively to roll the change out, so the version is really a promise about how disruptive the change is.
A patch (1.0.0 → 1.0.1) is a bug fix, performance tweak, or small UI change that touches nothing a tenant depends on. There are no data migrations, and existing tenants are upgraded automatically because there's nothing to break.
A minor (1.0.0 → 1.1.0) adds something — a feature, a table, a setting — while staying backward-compatible with 1.0.0. Existing tenants still auto-upgrade, though they can opt out, and the change may carry a schema migration since new tables or columns are additive and safe.
A major (1.0.0 → 2.0.0) is where breaking changes live: removing fields, changing behaviour, adding a required dependency. Because it can break a tenant who isn't ready, the platform doesn't push it silently — tenants get an upgrade prompt, the breaking change must be documented, and you may need a manual data migration to carry existing data across.
Dependency Versions
In manifest.json, specify version constraints:
"dependencies": {
"customers": "^1.0.0", // ✓ 1.0.0 to <2.0.0 (breaks on major)
"pricing": "~1.2.0", // ✓ 1.2.0 to <1.3.0 (breaks on minor)
"inventory": "1.0.0" // ✓ Exact version only
}
^1.0.0— Recommended. Breaks only on major version~1.0.0— Breaks only on minor version (more restrictive)1.0.0— Exact version only (most restrictive)
The trade-off is between staying current and staying still. A caret range picks up your dependency's fixes and features automatically and only stops at a major version; an exact pin never changes underneath you but also never gets a fix unless you bump it by hand. Caret is the right default for almost everything.
App States
Every app moves through a sequence of states over its life, from a private draft to — eventually — full removal. The states differ along two axes: whether the app is offered to new tenants, and whether existing tenants can still use it.
A Draft is an app still in development. It isn't listed in the Marketplace, only the developer can test it, and its version number doesn't increment yet — it's a workspace, not a release.
An Active app is the normal published state: listed in the Marketplace (if it has a billing.json), installable and usable by tenants, and eligible for upgrades or eventual deprecation.
A Deprecated app is on its way out but not gone. It's no longer offered to new tenants and disappears from the Marketplace, but tenants who already have it keep working — it's a grace period before the version reaches end-of-life.
An Archived app is fully retired. No one can install it and no new versions ship; existing tenants have it in an archived state and can only uninstall it. This is the terminal state.
Deployment
Deploy a New Version
Register the app from its manifest (only needed the first time, or when the manifest changes), then create and activate a deployment:
revenexx apps register --manifest manifest.json
revenexx apps create-deployment --function-id orders --activate true --code .
The platform:
- Validates
manifest.json,schema.json, etc. - Uploads your app code to the platform runtime
- Reads the version from
manifest.json(you specify major.minor.patch) - Registers the new version in the App Registry (Console)
- Publishes deployment event
Run revenexx apps <command> --help for the exact flags.
Auto-Upgrade Rules
Once a version is in the registry, how it reaches existing tenants depends on the kind of bump — the same gradient of caution as the version rules, now from the tenant's side.
Patch versions roll out to all tenants immediately and without any user action; the only visible trace is an app.upgraded lifecycle event carrying the from_version and to_version. Minor versions are offered rather than forced — existing tenants are notified and can opt in from the "Manage Apps" screen in Console, while any newly provisioned tenant simply starts on the latest minor. Major versions require an explicit yes: the tenant sees a "Critical update required" prompt, can read the breaking changes in the release notes, and is free to delay or refuse until they're ready. Nothing breaking ever happens to a tenant without their consent.
Lifecycle Events
The platform emits events when app lifecycle changes:
app.installed
Fired when a Tenant installs your app:
{
event: "app.installed",
app_name: "orders",
tenant_id: "acme-eu",
app_version: "1.0.0"
}
Your app can listen and initialize data:
// In manifest.json:
"events": {
"listens": ["app.installed"]
}
// In your function:
platform.events.on('app.installed', async (event) => {
// Initialize default settings
// Create system records
// Log installation
})
app.uninstalled
Fired when a Tenant removes your app:
{
event: "app.uninstalled",
app_name: "orders",
tenant_id: "acme-eu"
}
Clean up:
platform.events.on('app.uninstalled', async (event) => {
// Delete app-specific data (optional)
// Notify external systems
// Log removal
})
app.upgraded
Fired when your app is upgraded to a new version:
{
event: "app.upgraded",
app_name: "orders",
tenant_id: "acme-eu",
from_version: "1.0.0",
to_version: "1.1.0"
}
Run migrations or data transformations:
platform.events.on('app.upgraded', async (event) => {
// Run schema migrations (auto-handled by the platform)
// Backfill new fields
// Update configuration
})
tenant.provisioned
Fired when a new Tenant is created:
{
event: "tenant.provisioned",
tenant_id: "new-customer",
reason: "new_signup"
}
If your app is an Included App (bundled with the subscription), initialize it:
platform.events.on('tenant.provisioned', async (event) => {
// Initialize customer defaults
// Set up welcome data
})
Upgrade Workflow
1. Develop and Test (Local)
# Scaffold manifest.json + index.js, edit your code...
revenexx apps register --manifest manifest.json
2. Deploy and verify on a test tenant
Create a deployment but don't activate it yet, then verify it before it goes live. Run revenexx login --tenant <slug> (or revenexx tenants use <slug>) to point at your staging tenant first:
revenexx apps create-deployment --function-id orders --activate false --code .
revenexx apps list-deployments --function-id orders
# Verify the deployment, then activate it:
revenexx apps update-deployment --function-id orders --deployment-id <DEPLOYMENT_ID>
3. Deploy to Production
Against your production tenant, bump version in manifest.json, then create and activate a deployment:
revenexx apps create-deployment --function-id orders --activate true --code .
# Publishes to App Marketplace
# Existing Tenants notified of update
There's no --env flag — scope a deploy to an environment by selecting the tenant first with revenexx tenants use <slug> (or --tenant). Run revenexx apps <command> --help for the exact flags.
4. Tenants Upgrade
Patch: Auto-upgrade, no action Minor: Tenant notified, can opt-in Major: Tenant prompted, must accept
5. Post-Upgrade Verification
Finally, confirm the upgrade landed cleanly by opening the app in Cockpit: check that the views still render, that existing data survived the migration, that any new features are present, and that the browser console is free of errors. The migration that worked on staging almost always works in production too, but a one-minute look is cheap insurance.
Best Practices
Living safely with versioning comes down to one discipline: be honest with the version number, and let everything else follow from it.
Version every release, and version it truthfully. Increment manifest.json for every production deploy, and let the size of the bump match the size of the change — a breaking change is never a patch. The version is a promise to your tenants about how risky the upgrade is, and the auto-upgrade rules act on that promise; mislabel it and you'll either push a breaking change silently or nag people about a trivial fix.
Make change safe before you ship it. Document breaking changes in the release notes so a tenant deciding whether to accept a major upgrade has what they need, and test schema migrations on staging before production rather than discovering a bad migration against live data. Within a major version, hold the line on backward compatibility so auto-upgrading minors and patches stay genuinely safe.
Use the lifecycle events. Subscribe to app.installed, app.upgraded, and the rest to handle your own initialization, cleanup, and data backfills — they're the hooks that let an upgrade carry data forward instead of leaving it stranded.
Schema Migrations and Versioning
The platform handles schema changes automatically:
// Version 1.0.0
"orders": {
"columns": {
"id": { "type": "uuid", "primary": true },
"customer_id": { "type": "uuid" }
}
}
// Version 1.1.0 (added column)
"orders": {
"columns": {
"id": { "type": "uuid", "primary": true },
"customer_id": { "type": "uuid" },
"expected_delivery": { "type": "date", "required": false }
}
}
// Deploy v1.1.0 → the platform runs:
// ALTER TABLE orders ADD COLUMN expected_delivery DATE;
No manual migration files needed — git history is your migration log.
Next Steps
- Concept — Understand the declarative app model
- Billing & Events — Marketplace billing and events
- Cockpit Integration — UI extension points