Skip to content

Work / Charades / Wiki / Concepts

ota-rules

Conceptverified 2026-10-30

ota-rules

An OTA update ships JS only. ota.yml publishes to production users on every push to main, and runtimeVersion.policy is appVersion, so an update reaches installs whose version matches package.json. If a commit adds or upgrades a package carrying native code, installs on the current binary get JS that calls into native code they do not contain, and crash on the first call.

scripts/check-ota-safe.mjs enforces this: it diffs runtime dependencies between the last v* tag and HEAD, and fails the OTA if anything changed that is not on its JS_ONLY list. Unknown packages count as native. When it blocks you, cut a release instead. If the package really is pure JS, add it to JS_ONLY, after checking the tarball has no android/, ios/ or *.podspec.

An OTA carries its own copy of the store keys. EXPO_PUBLIC_* values are inlined into the JS bundle at bundling time, and an update rebuilds that bundle. eas build reads eas.json's build.production.env; eas update does not. It reads the EAS environment, and the .env.* files are gitignored so a CI runner never sees them either.

For a long time charades had no EAS environment variables at all, so every update ota.yml published inlined undefined for both RevenueCat keys. configurePurchases() refused to configure, and prices and purchases were dead on both platforms for anyone who received it, on a store binary that was perfectly fine. Nothing failed; the packs just rendered as locked, which is also what a user who owns nothing sees.

So: the two keys live in the EAS production environment (eas env:list production), eas update is always passed --environment production, and ota.yml refuses to publish if either key is missing. eas.json keeps its copy for eas build.

main must not bump the version. It used to, and because the bump landed before eas update, every update targeted a version created seconds earlier and never released: six consecutive updates reached zero users. It also spent an EAS build per merge (7 builds in 9 days against a 15/month tier, one of which shipped).

Steps: .context/ops/runbooks/release.md.