---
title: Live Updates for Ionic Portals Apps
description: Deliver Capawesome Cloud Live Updates to Ionic Portals apps on iOS and Android — install the provider, configure a manager per Portal, sign bundles, and publish updates.
tags:
  - Android
  - iOS
---

# Update Ionic Portals Apps

Live Updates work for [Ionic Portals](https://ionic.io/docs/portals){:target="_blank"} apps through the Ionic Live Update Provider SDK. A Portals host constructs one `LiveUpdateIonicManager` per Portal, Portals reads the bundle directory from that manager when it loads the web view, and the manager keeps the bundle current by syncing with Capawesome Cloud. The native host app stays untouched, and each web app that you ship in a Portal uses its own Capawesome Cloud app.

This page covers what is specific to Ionic Portals. For the general concepts, such as channels, bundles and the upload commands, follow [Get Started](setup.md) first. If you migrate from Ionic Appflow, the guide [Migrating Ionic Portals Live Updates to Capawesome Cloud](../../blog/posts/migrating-ionic-portals-live-updates-from-appflow-to-capawesome.md) walks through the reference app step by step.

## Prerequisites

- Ionic Portals `0.14.0` or later on iOS and Android. This is the first release built on Live Update Provider SDK `1.0.0`.
- `@capawesome/capacitor-live-update` version `8.7.0` or later. This is the first release that ships the manager.
- Android: JDK 21. Ionic Portals 0.14 depends on Capacitor 8, which is compiled for Java 21, so the host app has to build with JDK 21 with or without the Live Update SDK. The provider SDK requires `minSdkVersion` 24 or higher.
- iOS: Xcode 26 or later and a deployment target of iOS 15 or later.

## Installation

A native Portals host is not a Capacitor project, so there is no `npx cap sync` that wires up the SDK. Both platforms resolve it from a local `node_modules` instead. Add a `package.json` next to each native project and install it:

```json title="package.json"
{
  "name": "ios-app",
  "private": true,
  "dependencies": {
    "@capawesome/capacitor-live-update": "^8.7.0"
  }
}
```

Running `npm install` only downloads the SDK sources. It does not turn the host into a Capacitor project.

### iOS

Reference the base pod and the `IonicProvider` subspec in your `Podfile`. The subspec compiles the provider classes in and adds the `LiveUpdateProvider` pod:

```ruby title="Podfile"
platform :ios, '15.0'
use_frameworks!

target 'App' do
  pod 'IonicPortals', '~> 0.14'
  pod 'CapawesomeCapacitorLiveUpdate', :path => 'node_modules/@capawesome/capacitor-live-update'
  pod 'CapawesomeCapacitorLiveUpdate/IonicProvider', :path => 'node_modules/@capawesome/capacitor-live-update'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 15.0
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
      end
    end
  end
end
```

The `post_install` hook is required with Xcode 27, which refuses to build pods whose deployment target is below iOS 15. Ionic Portals pulls in three such pods. A Capacitor app gets this hook from the Capacitor CLI, a native host has to add it itself. Run `pod install` afterwards and open the `.xcworkspace`.

### Android

Include the SDK and the Capacitor core as Gradle modules from `node_modules`:

```groovy title="settings.gradle"
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('node_modules/@capacitor/android/capacitor')

include ':capawesome-capacitor-live-update'
project(':capawesome-capacitor-live-update').projectDir = new File('node_modules/@capawesome/capacitor-live-update/android')
```

The Android `package.json` also needs `@capacitor/android` as a dependency, because the SDK module depends on the local `:capacitor-android` module. Ionic Portals brings its own copy of the Capacitor core from Maven, so exclude it to keep a single core on the classpath:

```groovy title="app/build.gradle"
dependencies {
    implementation('io.ionic:portals:0.14.0') {
        exclude group: 'com.capacitorjs', module: 'core'
    }
    implementation project(':capacitor-android')
    implementation project(':capawesome-capacitor-live-update')
}
```

No provider dependency is needed. Ionic Portals brings `io.ionic:liveupdateprovider` at runtime, and the SDK only compiles against it.

## Configuration

Construct one `LiveUpdateIonicManager` per Portal and hand it to Portals where the Appflow configuration used to go. On iOS that is the `liveUpdateSource` of the `Portal`:

```swift
import CapawesomeCapacitorLiveUpdate
import IonicPortals

let manager = try LiveUpdateIonicManager(configuration: [
    "appId": "<APP_ID>",
    "channel": "production",
    "publicKey": "-----BEGIN PUBLIC KEY-----MIGf...IDAQAB-----END PUBLIC KEY-----"
])

let checkout = Portal(
    name: "checkout",
    startDir: "portals/checkout",
    liveUpdateSource: .provider(manager: manager)
)
```

On Android it is the `setLiveUpdateProviderManager(...)` call of the builder:

```java
import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicManager;

Map<String, Object> configuration = new HashMap<>();
configuration.put("appId", "<APP_ID>");
configuration.put("channel", "production");
configuration.put("publicKey", "-----BEGIN PUBLIC KEY-----MIGf...IDAQAB-----END PUBLIC KEY-----");

PortalManager.newPortal("checkout")
    .setStartDir("checkout")
    .setLiveUpdateProviderManager(new LiveUpdateIonicManager(this, configuration))
    .create();
```

The configuration accepts these keys:

| Key            | Required | Description                                                                                                       |
| -------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `appId`        | Yes      | The Capawesome Cloud app ID of the web app that this Portal shows.                                                |
| `channel`      | No       | The channel to sync, for example `production`. Without it the server picks the app's default channel.            |
| `publicKey`    | No       | The public key as a single-line PEM. The manager then rejects every bundle without a valid signature.             |
| `serverDomain` | No       | The API domain of the Capawesome Cloud server without scheme or path.                                             |
| `httpTimeout`  | No       | The request timeout in milliseconds.                                                                              |

Construction fails with `ProviderError.invalidConfiguration` when `appId` is missing or when `publicKey`, `serverDomain` or `httpTimeout` has the wrong type. A mistyped `channel` is ignored and falls back to the default. The manager stores its state per app ID and channel, so two Portals with the same values share one active bundle. The other options of the Capacitor configuration, such as `customId` or `autoUpdateStrategy`, do not apply in a Portals host.

### Sync on launch

A manager only syncs when the host asks for it. Call `syncProvider()` for every Portal with a manager once at app start, for example in `application(_:didFinishLaunchingWithOptions:)` on iOS and in `Application.onCreate()` on Android:

```swift
Task {
    for portal in [Portal.checkout, Portal.help] {
        _ = try? await portal.syncProvider()
    }
}
```

`sync()` returns the metadata of the new bundle, or `nil` when the Portal is up to date or the server answered with an error status. It throws when the device is offline, the request times out, or the download or signature check fails. A Portal that is already on screen keeps the current bundle and serves the new one on its next load.

## Code signing

We recommend code signing for every production app. Without a public key, the manager only verifies the checksum of a bundle. With a public key, it also verifies the RSA signature, so a bundle that was not signed with your private key is rejected even if someone manages to serve it.

Generate the key pair and upload signed bundles as described in [Sign Your Bundles](code-signing.md). The only difference in a Portals host is where the public key lives: pass it as `publicKey` in the manager configuration instead of the Capacitor configuration. Mark the channel as [protected](channels.md) in Capawesome Cloud so it only accepts signed bundles.

## Publish an update

Create one Capawesome Cloud app per web app that you ship in a Portal. Build the web app and upload the output to the channel the manager syncs:

```bash
npm run build
npx @capawesome/cli apps:liveupdates:upload --app-id <APP_ID> --path ./build --channel production --private-key ./private-key.pem
```

Ship the same build as seed content inside the native app, so a Portal has content before the first sync. The reference app copies the build output into the app bundle with an Xcode build phase on iOS and a Gradle copy task on Android.

## Differences to Capacitor apps

- **Sync**: Only the host triggers a sync, through `syncProvider()`. There is no automatic update strategy and no JavaScript API involved.
- **Rollback**: There is no `ready()` call and no automatic rollback. A bad bundle is replaced by publishing a new one or by [rolling back](rollbacks.md) the channel in Capawesome Cloud.
- **Native updates**: After a store update with a new version code or `CFBundleVersion`, every manager forgets its bundle and serves the seed content until the next sync.
- **Storage**: Bundles that a sync replaced are deleted on the next app launch, after no manager points to them anymore. Several Portals that show the same web app share one copy of the bundle.
- **Configuration**: `appId`, `channel`, `publicKey`, `serverDomain` and `httpTimeout` come from the manager configuration. Nothing is read from a Capacitor configuration file.

## Troubleshooting

Start with the general [Troubleshooting](troubleshooting.md) page. The following issues are specific to Ionic Portals:

- **iOS does not find `LiveUpdateIonicManager`**: The `IonicProvider` subspec is missing from the `Podfile`. The provider classes only compile with it.
- **Xcode reports a deployment target below iOS 15 for a pod**: The `post_install` hook from the [installation](#ios) is missing. Add it and run `pod install` again.
- **Android reports duplicate Capacitor classes**: Two copies of the Capacitor core are on the classpath. Exclude `com.capacitorjs:core` from `io.ionic:portals` and from every other Maven-published Capacitor plugin.
- **Construction fails with `appId must be configured.`**: The configuration has no `appId`. Every manager in a Portals host needs one.
- **A Portal runs the seed content after a store update**: This is expected. The manager resets on a version change and picks up the next matching bundle on the following sync.
- **A Portal still shows the old bundle after a sync**: The Portal was already on screen when the sync finished. It serves the new bundle on its next load, or after an app restart.

## Next steps

- [Sign your bundles](code-signing.md) — generate a key pair and upload signed bundles.
- [Manage channels](channels.md) — separate production and testing audiences.
- [Automate publishing in CI/CD](integrations/index.md) — upload bundles from your pipeline.
