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.