App Upgrades
App Upgrades
The Turn Apps framework supports upgrading apps to new versions while preserving important state like configuration and webhook URLs. This section covers how upgrades work and how to implement upgrade hooks in your app.
Overview
When you upload a new version of an app, the platform automatically manages version transitions:
- Automatic archiving - When you upload a new version, the previous version is automatically archived
- Single active version - Only one version of each app can be active at a time per number
- Upgrade existing installation - If the app is already installed, you can upgrade the existing installation to preserve the
app_install.uuid(keeping webhook URLs stable), configuration, and version history
For installed apps, you'll typically want to upgrade existing installations to maintain continuity for users who have integrated your app's webhook URLs into external systems.
What Gets Preserved During Upgrade
When upgrading an app, the following are preserved:
- App Install UUID - The unique identifier for this installation remains the same, which means webhook URLs (
/apps/{uuid}/webhook) continue working - Configuration - All config values are preserved by default (apps can migrate config via upgrade hooks)
- Version History - The system tracks up to 10 previous versions, enabling rollback if needed
The Upgrade Flow
- User uploads new version of an existing app
- System detects an existing installation with the same app name
- User chooses "Upgrade existing installation"
- If upgrading to a newer version:
- The current worker pool is stopped
- Version history records the old version and config snapshot
- AppInstall is updated to point to the new version (config preserved)
- The
upgradeevent runs with access toturn.app.*APIs for config migration - A new worker pool starts with the new code
- If downgrading to an older version:
- Similar process, but the
downgradeevent runs instead
- Similar process, but the
Implementing Upgrade Hooks
Apps can implement upgrade and downgrade events in on_event to migrate configuration between versions. This is particularly useful when:
- You've renamed configuration fields
- You need to transform config values for the new version
- You want to add metadata about the migration
The upgrade Event
Called when upgrading FROM an older version TO a newer version. The hook runs in the new app code with full access to turn.app.* APIs:
function App.on_event(app, number, event, data)
if event == "upgrade" then
-- data.from_version: version upgrading from (e.g., "1.0.0")
-- data.to_version: version upgrading to (e.g., "2.0.0")
turn.logger.info("Upgrading from " .. data.from_version .. " to " .. data.to_version)
-- Read current config using turn.app API
local config = turn.app.get_config()
-- Example: rename a field in v2.0.0
if config.old_api_endpoint then
config.api_url = config.old_api_endpoint
config.old_api_endpoint = nil
end
-- Add upgrade metadata
config.upgraded_from = data.from_version
config.upgraded_at = os.date("!%Y-%m-%dT%H:%M:%SZ")
-- Save the migrated config
turn.app.set_config(config)
return true
end
end
The downgrade Event
Called when downgrading FROM a newer version TO an older version. The hook runs in the old (target) app code with full access to turn.app.* APIs:
function App.on_event(app, number, event, data)
if event == "downgrade" then
-- data.from_version: version downgrading from (e.g., "2.0.0")
-- data.to_version: version downgrading to (e.g., "1.0.0")
turn.logger.info("Downgrading from " .. data.from_version .. " to " .. data.to_version)
-- Read current config using turn.app API
local config = turn.app.get_config()
-- Reverse the v2.0.0 field rename
if config.api_url then
config.old_api_endpoint = config.api_url
config.api_url = nil
end
config.downgraded_from = data.from_version
-- Save the migrated config
turn.app.set_config(config)
return true
end
end
Apps Without Upgrade Hooks
If your app doesn't implement upgrade/downgrade hooks, the existing configuration is preserved as-is during upgrades. This is the default behavior and works well for apps where config structure doesn't change between versions.
Version History and Rollback
The platform maintains a version history for each app installation, recording:
- Version string - The semantic version (e.g., "1.0.0")
- App definition UUID - Reference to the app definition
- Upgraded at - ISO 8601 timestamp of when the upgrade occurred
- Config snapshot - The complete configuration at upgrade time
This history is limited to 10 entries by default to prevent unbounded growth.
Rollback Capability
Users can rollback to the previous version from the app's settings page. When rolling back:
- The system retrieves the previous app definition from version history
- The config snapshot from that version is restored
- The downgrade hook runs in the previous version's code (if implemented)
- The worker pool restarts with the previous version
This provides a safety net when upgrades cause unexpected issues.
Automatic Rollback on Failure
The upgrade process includes automatic rollback to ensure app stability. If either of the following occurs during upgrade:
- Upgrade hook returns
false- The app'supgradeevent handler indicates failure - Pool fails to start - The new app code fails to initialize
The platform will automatically:
- Restore the previous app definition
- Restore the config snapshot from before the upgrade
- Remove the version history entry that was just added
- Restart the worker pool with the previous version
- Return an error message explaining what happened
This ensures that apps remain functional even when upgrades fail. The automatic rollback preserves:
- The original app version and code
- All configuration values from before the upgrade attempt
- Full functionality (the pool restarts with the working version)
Example of handling upgrade failures:
function App.on_event(app, number, event, data)
if event == "upgrade" then
-- Validate that required external services are available
local ok, err = validate_external_dependencies()
if not ok then
turn.logger.error("Upgrade failed: " .. err)
-- Returning false triggers automatic rollback
return false
end
-- Proceed with config migration
local config = turn.app.get_config()
-- ... migration logic ...
turn.app.set_config(config)
return true
end
end
Best Practices for Upgrades
-
Always Test Upgrade Paths - Before releasing a new version, test upgrading from the previous version
-
Make Upgrade Hooks Idempotent - If an upgrade hook runs multiple times, it should produce the same result
-
Handle Missing Fields Gracefully - Don't assume fields exist; use defaults:
local timeout = migrated_config.timeout or 30 -
Log Upgrade Actions - Help users understand what changed during upgrade:
turn.logger.info("Migrated api_endpoint to api_url") -
Document Breaking Changes - Use
get_app_info_markdownto communicate version changes:elseif event == "get_app_info_markdown" then
return [[
# My App v2.0
## Upgrade Notes
- Config field `api_endpoint` was renamed to `api_url`
- The upgrade hook automatically migrates this for you
]]
end -
Preserve Semantic Versioning - Use proper semver to communicate the type of changes:
- MAJOR: Breaking changes requiring config migration
- MINOR: New features, backwards compatible
- PATCH: Bug fixes