Upwell

Quick start

Publish a policy, wrap your app, and let Upwell handle the update flows.

This guide gets you from zero to working store and OTA prompts. It assumes you've installed Upwell and rebuilt your app.

Publish a policy

Create a policy.json and host it anywhere that serves static files: a CDN, S3, GitHub Pages, or your own API. Upwell fetches it on launch and on foreground.

policy.json
{
  "$schema": "https://upwell.run/schemas/policy.schema.json",
  "schemaVersion": 1,
  "android": {
    "latest": { "version": "3.4.0", "build": 340, "releasedAt": "2026-10-18" },
    "minimum": { "version": "3.0.0", "build": 300 },
    "recommendAfterDays": 2,
    "ota": {
      "latestRuntimeVersion": "3.4.0",
      "applyMode": "nextLaunch",
      "strandedRequireAfterDays": 14
    }
  },
  "ios": {
    "appStoreId": "1234567890",
    "latest": { "version": "3.4.0", "build": "1", "releasedAt": "2026-10-18" },
    "minimum": { "version": "3.0.0", "build": "1" },
    "ota": { "latestRuntimeVersion": "3.4.0", "applyMode": "prompt" }
  }
}

The policy says:

  • The newest store build is 3.4.0. Installs below it get a recommendation once the build is 2 days old.
  • Installs below 3.0.0 must update.
  • OTA updates target runtime 3.4.0. Installs on another runtime are stranded, and they must update 14 days after 3.4.0 shipped.

The $schema line gives you autocomplete and validation in your editor. Every field is described in the policy reference.

Wrap your app

Put UpwellProvider near the root of your app and render UpwellUI once, as its last child:

App.tsx
import { UpwellProvider } from 'upwell/react';
import { expoUpdatesAdapter } from 'upwell/expo-updates';
import { UpwellUI } from 'upwell/ui';

export default function App() {
  return (
    <UpwellProvider
      policy="https://example.com/upwell/policy.json"
      ota={expoUpdatesAdapter()}
    >
      <RootNavigator />
      <UpwellUI />
    </UpwellProvider>
  );
}

That's all the code you need. Upwell checks when the provider mounts and every time the app comes back to the foreground (at most once a minute). <UpwellUI /> renders whatever the current decision calls for:

DecisionUI
store.recommendedUpdatePrompt: "Update" / "Later"
store.downloadingDownloadProgressBanner
store.readyToInstall, ota.ready (prompt mode)RestartSnackbar
store.requiredForceUpdateScreen, which can't be dismissed
An OTA update was just appliedUpdatedSheet, if you publish releases.json

Try every state

You don't need to publish a store build to see the flows. Add the dev menu to a debug screen and tap through the scenarios:

DebugScreen.tsx
import { UpwellDevMenu } from 'upwell/ui';

export function DebugScreen() {
  return <UpwellDevMenu style={{ flex: 1 }} />;
}

On Android, the "recommended" and "required" scenarios run Play's FakeAppUpdateManager inside the native module, so the real Kotlin code path runs. See Testing and simulation.

Ship

When you release a new store build, update latest in the policy. When you need everyone off an old version, raise minimum. When you change native code and bump the runtime, update ota.latestRuntimeVersion. You never need a new app release to change how Upwell behaves.

Prefer your own UI?

Skip <UpwellUI /> and read the decision yourself with useAppUpdate():

import { useAppUpdate } from 'upwell/react';

function UpdateBanner() {
  const { decision, startUpdate, dismiss } = useAppUpdate();
  if (decision.kind !== 'store.recommended') return null;
  return <MyBanner title={decision.message?.title} onUpdate={startUpdate} onLater={dismiss} />;
}

See Build your own UI.

Next steps

On this page