Docs / SDK support levelsSDK downloadsGitHub ↗

SDK support levels

This page lists what each PushHub SDK supports, on each platform, for remote config, instant refresh and signed config. For each SDK it also lists the setup you must do yourself and how far each claim has been verified.

It describes the release candidate: Android 0.1.0-beta.7, JavaScript 2.0.0-beta.4, and Unity, Flutter, React Native, Godot and Unreal 0.1.0-beta.3. It covers them as they are packaged for the downloads page and PushHub's Maven repository. iOS is not part of this release and stays at the published 0.1.0-beta.1.

Every statement comes from evidence: a device run of the packaged artifacts on 2026-10-05 (an Android 15 emulator talking to the production API), clean-consumer builds of each package, unit tests, and reading the packaged code. The device run is in the device validation report. Including the Android core in a wrapper doesn't, by itself, give that wrapper background refresh or signed config. A wrapper row claims a feature only when it was shown through that wrapper's own API and packaging.

How far each claim is verified, strongest first:

  • Device-verified: the packaged artifact ran on a device or emulator against the live service, with logs captured.
  • Clean consumer: a new project built from the packaged artifact and the documented install steps alone.
  • Tested in CI: compiled and unit-tested by the SDK pipeline.
  • Compiled: builds, but has no tests.
  • Code review: read from the code or the packaged API; not run.

Summary

SDK · platform Release candidate Remote config Instant refresh Signed config Manual dependencies Verified
Android (native) 0.1.0-beta.7 · Beta Full Automatic, also when Android starts the app's process for it, with a bundled pushhub.json or PushHub.configure from code Yes, once you set trusted keys: checked at fetch, activation, startup and key change; keys stored on the device None from PushHub's Maven repository (one repository line, one dependency). With the AAR download: six dependencies Device-verified with the packaged AAR: 12 of 12 scenarios (evidence); CI-tested
iOS (native) Not in this release (published 0.1.0-beta.1 · Beta) Not in the published SDK Not in the published SDK Not in the published SDK Swift Package Manager, or CocoaPods pinned to a commit Preview, unreleased. Remote config, refresh and signing exist in source and pass XCTests in CI on macOS. No iOS-target build or device run
JavaScript 2.0.0-beta.4 · Preview Full Your code calls config.handleRefreshMessage Yes, once you set signing keys None: npm install <tarball URL> Clean consumer in Node, with the signing test vector; browser run of the packaged tarball in Chromium 152, 28 of 28 checks, against a cross-origin stand-in that signs with real ES256 keys (evidence); CI-tested. Not yet run against the live service with a real installation
Unity · Android 0.1.0-beta.3 · Preview Full, via the Android core Not validated through Unity No None for the core (PushHub.androidlib declares it); Firebase values and player settings Package contents and the bridge check on the packaged AAR (37 of 37). No editor build or device run
Unity · iOS 0.1.0-beta.3 · Preview Defaults only No No — Not compiled
Flutter · Android 0.1.0-beta.3 · Preview Full, via the Android core Not validated through Flutter No None (the plug-in adds PushHub's Maven repository) Clean consumer (flutter build apk). Device run not done yet
Flutter · iOS 0.1.0-beta.3 · Preview Needs the unreleased iOS SDK Needs the unreleased iOS SDK No PushHub pod pinned to a commit Not compiled
React Native · Android 0.1.0-beta.3 · Preview Full, via the Android core Not validated through React Native No None (the module adds PushHub's Maven repository); JDK 17 Clean consumer (gradlew assembleDebug, new architecture). Device run not done yet
React Native · iOS 0.1.0-beta.3 · Preview Needs the unreleased iOS SDK Needs the unreleased iOS SDK plus AppDelegate code No PushHub pod pinned to a commit Not compiled
Godot · Android 0.1.0-beta.3 · Preview Full, via the Android plug-in Not validated through Godot No None (the zip ships the AAR; the export plug-in declares its dependencies); Godot 4.5+ Package contents, a Gradle build shaped like an export, the bridge check (37 of 37). No Godot export or device run
Godot · iOS — Not available (there is no iOS plug-in) No No — —
Unreal · Android 0.1.0-beta.3 · Preview Full, via JNI Not validated through Unreal No None (the APL adds PushHub's Maven repository and the core) The APL's Gradle additions build; JNI descriptors match the AAR (37 of 37). No engine build or device run
Unreal · iOS 0.1.0-beta.3 · Preview Needs a framework you build on macOS against the unreleased iOS SDK; otherwise defaults only Needs game code No — Not compiled

What "Not validated" means for the wrappers. Each wrapper's configure hands your values to the Android core 0.1.0-beta.7. On a device, that core:

  • stores those values;
  • configures itself in a process Android starts for a refresh;
  • fetches and activates the refresh. Device-verified with a native app configured from code, the way the wrappers configure.

That path hasn't been run through Unity, Flutter, React Native, Godot or Unreal. A device run of the Flutter and React Native packages is the next step: it was prepared, but the validation host ran out of disk space. Until a wrapper's own run shows it, don't rely on refreshes reaching a wrapper app that isn't running. Keep a fetch on load.

Signed config

Each SDK below was checked in its own code and packaged API, not inferred from which Android library it uses.

  • Android (native) 0.1.0-beta.7: supported, device-verified.
    • Call PushHub.config.setTrustedSigningKeys(mapOf(keyId to publicKeySpki)). From then on a config is used only when its X-PushHub-Signature is a valid ES256 signature, by a trusted key, of the exact response bytes.
    • The keys are stored on the device. A process Android starts for a refresh enforces them even though your code never ran there.
    • On the device, against the server's signing-key flow:
      • Fresh fetches: a response signed by a trusted key is accepted. An unsigned response, or one signed by a key the app doesn't trust, fails with a signature error, and the values stay.
      • Cached values: a config activated before you set keys isn't served after a restart; the in-app defaults are served instead. Neither is a stored config whose bytes were changed on the device.
      • Process restarts: a refresh into a process Android started accepts a config signed by a stored key. It rejects one signed by another key, and one that isn't signed.
      • Key rotation: preparing the next key, trusting both and rotating never falls back to defaults. Removing the old key stops serving a config it signed, and the next fetch skips the minimum interval.
    • Whenever a stored config isn't trusted, the getters return your in-app defaults and the next fetch skips the minimum interval.
  • JavaScript 2.0.0-beta.4: supported. Set remoteConfig.signingKeys or call config.setSigningKeys(...). The same rules apply, and the keys are saved in the configured storage. Tested in CI; a clean consumer verifies the published test vector and rejects a one-byte change.
    • In a browser: the packaged tarball, in Chromium 152 with the browser's own Web Crypto, passed these checks across page reloads:
      • Fresh fetches: a trusted signature is accepted. An unsigned, tampered or wrong-key response is rejected.
      • Cached values: an unsigned saved config isn't served once keys are set.
      • Restarts: a client that never sets the keys still enforces the saved ones.
      • Key rotation: a prepared key, then rotation, then removing the old key.
    • Your API's CORS headers: a browser can read the signature only when the response's CORS headers expose X-PushHub-Signature. PushHub's API does. A proxy in front of it that drops the header makes every config fail the check.
  • iOS: not in the published SDK. The next iOS release adds PushHub.config.setTrustedSigningKeys(_:) with the same rules. It exists in source and passes XCTests on macOS, but it is unreleased and not validated on a device.
  • Unity, Flutter, React Native, Godot, Unreal: not supported.
    • None of the packaged APIs has a way to set trusted keys, and no bridge passes keys to the Android core. This was checked in the downloaded packages.
    • The Android core checks signatures only if your own Android code sets the keys. That route isn't documented or tested with any wrapper.
    • If you turn on Config signing for an environment, players on these SDKs still accept unsigned config.

Earlier versions. In Android 0.1.0-beta.6 and JavaScript 2.0.0-beta.3, keys were held in memory only. A fetch made before the keys were set wasn't checked and could be activated later, and on Android a refresh into a process Firebase started wasn't checked (device-verified, F2). Upgrade to 0.1.0-beta.7 and 2.0.0-beta.4.

Wherever signing is supported: replay isn't prevented. An older genuine signed response can be resent.

Instant refresh

A refresh is a silent push that tells the device to fetch and activate the newest config. PushHub only sends one when you choose Notify devices while publishing, and only to installations whose SDK handles it silently:

Installation reports Gets refreshes from
Android core (native Android, and Unity, Flutter, React Native, Godot and Unreal on Android) 0.1.0-beta.6
iOS 0.1.0-beta.2 (the next iOS release)
JavaScript 2.0.0-beta.3

Older releases pass the message to the app, which may show it as a notification, so they aren't sent one. The same goes for an SDK that isn't listed or a version PushHub can't read. Those devices pick up the new config on their next normal fetch. The remote_config.refresh_sent audit entry counts them as excludedUnsupportedSdk.

On a device with the packaged Android core 0.1.0-beta.7, the server sent the refresh, the device received it, fetched the new version (a 200 on the config endpoint) and activated it. The figure in each row is the time from the server sending the refresh to activation on the device:

App state Android 0.1.0-beta.7 (device-verified)
In the foreground Worked (under 0.1 s)
In the background Worked (1.1 s)
Process killed by Android, bundled pushhub.json Worked (5.8 s, process started by Firebase)
Process killed by Android, configured with PushHub.configure from code Worked (1.6 s): the core configured itself from the values the app stored
Notification permission not granted or denied by the player Worked
Data-processing consent set to Denied by your app Nothing is fetched (by design; see below)
Force-stopped (Settings → Force stop) Not delivered: Android doesn't deliver messages to a force-stopped app

A refresh the app couldn't fetch is remembered. For example, the network dropped while the refresh arrived. From Android 0.1.0-beta.7 and JavaScript 2.0.0-beta.4, the next fetch skips the minimum fetch interval until the device has that version. Device-verified on Android: a normal fetch on load, 97 seconds after the previous one, went to the server and activated the missed version. In a browser, a refresh handed to config.handleRefreshMessage during an outage was remembered across a reload, and the next fetch inside a 3600-second interval went to the server. Without such a refresh, the interval (900 seconds by default) still applies. A force-stopped app never receives the message, so for it the next fetch after the interval, or a forced fetch, picks up the version. Always keep a fetch on load.

Other SDKs:

  • Unity, Flutter, React Native, Godot, Unreal: not validated through the wrapper (see the summary).
  • iOS: needs the next iOS release, the Background Modes → Remote notifications capability, and an AppDelegate call to PushHub.handleBackgroundNotification.
  • JavaScript: has no silent push channel of its own. Pass the refresh message from your push integration to config.handleRefreshMessage.

Notification permission and data-processing consent are separate

  • Notification permission is the player's OS setting (POST_NOTIFICATIONS on Android 13 and later). It only decides whether notifications are shown. Config fetches and silent refreshes work without it: the device stays registered, and a refresh into a killed app is received, fetched and activated. (Device-verified on 0.1.0-beta.7.)
  • Data-processing consent is your app's call to setConsent. With consent Denied, the SDK makes no config request: your own fetch fails on the device before any network call, and the server deactivates the device so refreshes aren't sent to it. An API log over the whole window showed no request. The last values stay in use. Granting consent again resumes fetching. (Device-verified on 0.1.0-beta.7.)

Don't map a denied notification permission to consent Denied: that would stop remote config for players who only turned off notifications.

Other verified behaviour (Android core 0.1.0-beta.7, device-verified)

  • Defaults: before the first fetch, getters return your in-app defaults with source default.
  • Fetch and activate: the new version's values become active with source remote. An unchanged config is answered with 304 Not Modified. A normal fetch inside the minimum interval makes no request.
  • Restart: after a force-stop and a cold start with no network, the last activated values are available immediately, before any network call.
  • Offline: a fetch with no network fails cleanly. There's no crash, and the values don't change.
  • Targeting: conditions on the device context PushHub stores (app version and build, OS version, locale, country, time zone) and on testDevice select the right values. Values switch when a device's test-device status changes.
  • Rollback: rolling back serves the older values under a new version number.
  • Code-configured apps: calling PushHub.configure again with the same values in a process the SDK already configured for a refresh doesn't throw.

Setup you must do yourself

These steps apply from Android 0.1.0-beta.7, JavaScript 2.0.0-beta.4 and the 0.1.0-beta.3 Unity, Flutter, React Native, Godot and Unreal packages. Each package's dependencies install from what PushHub publishes, without Maven local, manual AAR copies or Gradle-template edits.

Where the Android core comes from. com.kemegames.pushhub:pushhub-android is published to PushHub's Maven repository at https://docs.pushhub.kemegames.com/maven. Its POM declares every runtime dependency, and every file has .sha1, .sha256 and .sha512 checksums. Published versions are never changed. Flutter, React Native and Unreal resolve the core from there. Unity and Godot ship the same AAR file inside their packages.

Android (native)

  • Add the repository in settings.gradle(.kts) (maven { url = uri("https://docs.pushhub.kemegames.com/maven") }) and implementation("com.kemegames.pushhub:pushhub-android:0.1.0-beta.7"). Gradle adds the dependencies from the POM. The device validation built its app exactly this way, from the packaged repository.
  • Or use the AAR download and declare its dependencies yourself:
    • com.google.firebase:firebase-messaging:24.1.0
    • androidx.core:core-ktx:1.15.0
    • androidx.security:security-crypto:1.1.0-alpha06
    • androidx.work:work-runtime-ktx:2.10.0
    • org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0
    • org.jetbrains.kotlin:kotlin-stdlib:2.1.0
  • Add the Firebase configuration, either the PushHub config bundle or google-services.json.
  • Use compileSdk 35 or later, minSdk 23 or later, and Java 17.

iOS (native). Install from git with Swift Package Manager, or with CocoaPods pinned to a commit: pod 'PushHub', :git => 'https://github.com/kemegames-studio/pushhub.git', :commit => '<sha>'. No release tag contains the podspec yet, so don't pin a tag. Remote config, refresh and signing need the next iOS release.

JavaScript. npm install https://docs.pushhub.kemegames.com/downloads/pushhub-js-2.0.0-beta.4.tgz. The package isn't on the npm registry, so npm install pushhub-js fails.

Unity. The package carries its Android core. The steps below come from the package's contents and Gradle builds shaped like Unity's export; a Unity editor build hasn't been run yet.

  • Add the package from the tarball, kept inside the project (for example Packages/vendor/). It contains pushhub-android-0.1.0-beta.7.aar and PushHub.androidlib, an Android Library plug-in that declares the core's dependencies from google() and mavenCentral(). No custom Gradle templates are needed for them.
  • If you set up an earlier version by hand, remove the AAR from Assets/Plugins/Android/ and the PushHub lines from mainTemplate.gradle, or the build fails with duplicate classes.
  • If the build reports that android.useAndroidX isn't enabled, turn on Custom Gradle Properties Template and add android.useAndroidX=true. Not yet checked in the editor.
  • Add your Firebase values as Android string resources (google_app_id, gcm_defaultSenderId, google_api_key, project_id, google_storage_bucket), or use the Firebase Unity SDK.
  • Player Settings: Minimum API Level 23 or higher, Target API Automatic (35 or later installed), IL2CPP, and ARM64 (add x86_64 for emulators).
  • Set Managed Stripping Level to Minimal, or add <linker><assembly fullname="Keme.PushHub" preserve="all"/></linker> in Assets/link.xml.
  • Don't add pushhub.json as an Android asset; PushHub.Configure would then fail with "already configured".
  • If another Firebase messaging plug-in is installed, PushHub's refresh messages may go to it instead.

Flutter and React Native.

  • Android: nothing to install. The package's Gradle file adds PushHub's Maven repository (limited to com.kemegames.pushhub) and the core. If your app's settings.gradle sets RepositoriesMode.FAIL_ON_PROJECT_REPOS (neither template does), add the repository there.
  • Flutter: unzip the plug-in into your app and add it as a path dependency. React Native: npm install ./kemegames-pushhub-react-native-0.1.0-beta.3.tgz; the Android build wants JDK 17.
  • Add the Firebase configuration.
  • iOS needs the PushHub pod from git, pinned to a commit. React Native iOS also needs the AppDelegate token forwarding described in its guide.

Godot.

  • Copy addons/pushhub from the zip. It ships the AAR as addons/pushhub/android/pushhub-release.aar, and the export plug-in adds exactly the POM's dependencies.
  • Android exports need Godot 4.5 or later: those dependencies need compileSdk 35, and the 4.3 and 4.4 build templates compile against 34. Use a Gradle build with minSdk 23.
  • Add the Firebase configuration.
  • There is no iOS plug-in.

Unreal.

  • Android: nothing to install. PushHub_APL.xml adds PushHub's Maven repository and com.kemegames.pushhub:pushhub-android:0.1.0-beta.7 to the generated Gradle project. An Unreal build hasn't been run yet.
  • Add the Firebase configuration.
  • For iOS, build the PushHubUnreal xcframework on macOS against the next iOS SDK release.

How the installation is checked. scripts/sdk-consumer-validation/ builds a new consumer project from each package and its documented steps alone. Native Android, Flutter and React Native build an app. JavaScript runs Node smoke tests with the signing test vector. Unity, Godot and Unreal are assembled and checked up to the editor build, which the validation host can't run. A clean-consumer build is not a device run: the wrappers stay Preview.

Known issues

  • iOS: the next iOS SDK release, which adds remote config, refresh and signing, is not published. Every iOS wrapper path waits on it.
  • Wrappers on Android: instant refresh while the app isn't running, and refresh in general, haven't been shown through any wrapper's own API. Signed config can't be configured from any wrapper. (code review of the packaged APIs)
  • Unity iOS can't register devices in 0.1.0-beta.2 or 0.1.0-beta.3, and remote config there returns defaults only. (code review)
  • Force-stopped Android apps don't receive refreshes. The next fetch after the minimum interval, or a forced fetch, picks up the version. (device-verified)
  • Fixed on the server: refresh pushes reached apps on older SDKs.
    • What happened: Notify devices used to send the silent refresh to every targeted device, including devices whose SDK predates silent refresh. Such an SDK, for example Android 0.1.0-beta.3, handed the message to the app's own message handler, which could show it as a notification. This was observed on a test phone.
    • Now: the server sends refreshes only to the SDK versions listed under Instant refresh above. Other devices pick up the config on their next fetch.
  • Fixed in Android 0.1.0-beta.7 and JavaScript 2.0.0-beta.4 (device-verified on Android; unit-tested in JavaScript):
    • Refresh dropped for code-configured Android apps whose process wasn't running (F1).
    • Configs that no trusted key verified could become active (F2).
    • A missed refresh waited out the minimum fetch interval (F3).
  • Flutter on Android: requestPermission() isn't implemented. (code review)
  • React Native on Android: requestPermission() resolves before the player answers. (code review)
  • React Native on Android, package 0.1.0-beta.2 and earlier: a new-architecture app (React Native 0.76 and later) fails to build with "'PushHubSpec.h' file not found", because the module didn't run codegen. Fixed in 0.1.0-beta.3. (clean-consumer build, React Native 0.87.1)
  • Unity: (code review)
    • StartAsync blocks the main thread.
    • A failed fetch returns the same false as "no change".
    • TrackAsync fails on Android with packages up to 0.1.0-beta.2: the Android core's track bridge returns a value where CallStatic expects void, so the call throws NoSuchMethodError. Fixed in the core 0.1.0-beta.7 that package 0.1.0-beta.3 ships: the bridge check against the AAR inside the packaged tgz passes (37 of 37, track returns void). No Unity build has run it yet.