Upwell

Hosting the policy

Where to put policy.json, how caching works, and what happens when it can't be fetched.

The policy is a static JSON file. Anything that serves HTTPS works.

HostNotes
A CDN (CloudFront, Cloudflare, Fastly)Set a short cache TTL (e.g. 60 s), so a change reaches devices quickly
S3, R2, GCS bucketMake the object public, or use headers for auth
GitHub Pages / raw filesEasy to review policy changes in pull requests. Pages caches for about 10 minutes
Your APIReturn the JSON from any endpoint. You can generate it, e.g. from your release pipeline
Firebase Remote Config, LaunchDarkly, …Use a custom source

Fetching

Pass a URL and Upwell uses jsonUrl() under the hood:

<UpwellProvider policy="https://cdn.example.com/upwell/policy.json">

Or configure it:

import { jsonUrl } from 'upwell';

<UpwellProvider
  policy={jsonUrl('https://api.example.com/upwell/policy', {
    headers: { Authorization: `Bearer ${token}` },
    cacheBust: true,
  })}
/>
jsonUrl optionDefault
headers—Extra request headers
cacheBustfalseAppends ?_=<timestamp> to skip CDN caches. Off by default because it breaks signed URLs
fetchglobal fetchA custom fetch implementation

Requests always send Cache-Control: no-cache, so the HTTP cache on the device doesn't serve stale copies. They time out after timeoutMs (default 10 s).

Last known good

Every valid policy is cached on the device. When a fetch fails (offline, timeout, HTTP error) or returns an invalid policy:

  1. Upwell uses the cached copy and decides normally.
  2. state.policyError holds the reason, and the trace says using last known good.
  3. Only when there's no cached copy either, and nothing else to do (such as an OTA update), is the decision error.

So a CDN outage never blocks your users, and a broken policy edit doesn't take effect.

A valid but wrong policy does take effect

Validation catches typos and impossible values (like minimum above latest), but not a minimum you raised by mistake. Review policy changes like code, and check them with simulation or explain() first.

Bundling a fallback

To have a policy before the first fetch ever succeeds, bundle one and fall back to it:

import { jsonUrl, type PolicySource } from 'upwell';
import bundled from './policy.json';

const remote = jsonUrl('https://cdn.example.com/upwell/policy.json');

const policy: PolicySource = {
  async fetch(signal) {
    try {
      return await remote.fetch(signal);
    } catch {
      return bundled as never;
    }
  },
};

Because the source then succeeds even when the network fails, the bundled copy wins over the cached one in that case. Keep it conservative, for example only a minimum you're sure about.

One file or many

One file with an android and an ios section is the simplest. If you prefer separate files per platform or environment, pick the URL at runtime:

const env = __DEV__ ? 'staging' : 'production';

<UpwellProvider policy={`https://cdn.example.com/upwell/${env}/policy.json`} />;

Release checklist

  1. Ship the new build to the store and wait until it's live (and rolled out, on Play).
  2. Update latest (with releasedAt) in the policy.
  3. Once the first OTA update for the new runtime is published, update ota.latestRuntimeVersion.
  4. Raise minimum only when you really need to block older builds.

On this page