Store builds and submissions go through EAS, in the cloud or with --local on a Mac with Xcode and the
Android SDK. Run those commands with npx eas-cli@latest (or a global eas). Over-the-air (OTA)
updates come from our own server, update-server/, and are signed on the publisher's machine.
eas login, theneas initto link the project to the Expo organization. This addsownerandextra.eas.projectIdtoapp.json.- Deploy the update server at
https://updates.shroudwallet.com(seeupdate-server/README.md). That URL is compiled into the app (updates.urlinapp.json,EXUpdatesURLinios/Shroud/Supporting/Expo.plist,expo.modules.updates.EXPO_UPDATE_URLinandroid/app/src/main/AndroidManifest.xml), so changing it takes a new store build. - Optional: every network ships its own indexer addresses in
modules/network.ts. To override one, set the matchingEXPO_PUBLIC_INDEXER_*variable (the names are in.env.example):- Store builds read it from the
development,previewandproductionEAS environments (eas env:create). - OTA updates ignore
.envfiles and take only theEXPO_PUBLIC_*variables in the environment of the command that publishes them. Publish undereas env:exec <environment>(see OTA updates) so an update gets the same overrides as the builds it targets.
- Store builds read it from the
OTA updates are signed; builds reject any update that is not.
- The certificate is committed at
certs/certificate.pem(valid until 2036-09-27) and compiled into every release build. - The private key is not in the repository. It must be kept in the team's password manager or vault. Anyone with the key can push code to every install, and a lost key means no more updates until a new build with a new certificate is in users' hands.
npm run ota:publishandnpm run ota:rollbacktake--private-key <path to private-key.pem>. They refuse a key that doesn't match the certificate, and they check every signature before uploading.- The update server never sees the key, so someone who takes over the server can't make the app run
code we didn't sign. They can withhold updates, and they can serve any update or roll-back ever
signed for a runtime version, on any channel: signatures aren't tied to a channel and can't be
revoked. Treat everything you sign as if it went to production, including preview updates and
updates you later roll back. See
update-server/README.md. - Only release builds carry the certificate. Debug builds (development clients) leave it out, so they load unsigned manifests from Metro and nobody needs the key to develop.
eas build --platform ios --profile production --local # without --local, EAS builds in the cloud
eas build --platform android --profile production --local
eas submit --platform ios --profile production
eas submit --platform android --profile production--localbuilds on this machine, one platform at a time.- Each build asks for updates on the
channelof its profile ineas.json:production,preview(also thesimulatorprofile) ordevelopment. EAS Build only sets that for EAS Update, soscripts/eas-build-pre-install.shdoes it for our server. Builds made outside EAS ask forproduction. The channel doesn't change the runtime version (seefingerprint.config.js). - Build numbers are managed by EAS (
appVersionSource: remote) and increase automatically. The user-facing version isMARKETING_VERSION(iOS) andversionName(Android). - Google Play does not accept an app's first upload through the API: upload the first AAB by hand in
Play Console, then use
eas submit.
An update can only change JavaScript and assets, and only reaches builds with the same runtime
version. The runtime version is a fingerprint of everything native: android/, ios/, native
dependencies, the Rust crate and the codegen specs (see fingerprint.config.js). If a change moves
the fingerprint, it needs a new store build instead:
npx expo-updates fingerprint:generate --platform ios # and --platform androidPublish to preview first, check it on a preview build, then publish to production. eas env:exec
runs the publish with the EXPO_PUBLIC_* variables of that EAS environment, the same ones its
store builds were made with:
eas env:exec preview 'npm run ota:publish -- --channel preview --message "..." \
--private-key <key> --upload deploy@updates.shroudwallet.com:/srv/shroud-updates'
eas env:exec production 'npm run ota:publish -- --channel production --message "..." \
--private-key <key> --upload deploy@updates.shroudwallet.com:/srv/shroud-updates'- Publish from a clean checkout. The script refuses uncommitted changes unless you pass
--allow-dirty, and signs the commit and message into the update's manifest. - Leave out
--uploadto stage the files underbuild/ota/stageand look at them first. With it, the script first asks the server for the live update, and stops if the new one would be older (this machine's clock is behind): apps would ignore it. - If the server's SSH key is restricted with rrsync, the upload target is
deploy@updates.shroudwallet.com:(seeupdate-server/README.md). - Roll back to the code built into the app:
npm run ota:rollback -- --channel production --private-key <key> --upload deploy@updates.shroudwallet.com:/srv/shroud-updates. Run it from the commit the builds were made from, or pass--runtime-version-iosand--runtime-version-android. It stops if the server has nothing live for those runtime versions, which usually means the wrong checkout. - To go back to an earlier update, check out its commit and publish it again.
- The app checks for updates on every launch. When it finds one, it holds the user on an "Updating" screen until the download is done, then asks them to restart into it. If the download fails, or is still going after a minute, they can carry on with the version they have, and the next launch tries again. A roll-back asks for the restart straight away. Every update interrupts people this way, so don't publish one that changes nothing.
- The screen takes the app's place whenever an update turns up, not only before the app shows. The launch doesn't wait for the check, so on a slow connection someone may already be at the unlock prompt, or halfway through a send, when the download starts. That screen and anything typed into it are lost; the wallet isn't. The same goes for a first minimum-build answer (below).
- That check, and the minimum-build check below, go to
updates.shroudwallet.comover the regular internet, including for users in Tor-only mode. The server keeps no access logs.
To stop old builds from running, set the oldest build number each platform may run. A build below it shows "Update required" with a button to its store, and nothing else, until it's updated:
node scripts/ota/minimum-build.mjs --channel production --ios 14 --android 12 \
--upload deploy@updates.shroudwallet.com:/srv/shroud-updates- A platform you leave out keeps its live minimum. Before uploading, the script shows the live and
new numbers and asks you to type
yes;--yesskips the question. - These are EAS build numbers (iOS
CFBundleVersion, AndroidversionCode), not the version people see. Find them in App Store Connect, Play Console oreas build:list. 0 means no minimum. - Raise a minimum only once a build that meets it is live in its store, at 100% of a staged rollout on Google Play. Anyone the store can't update yet is locked out.
- Never raise it past the OS versions the blocked builds support. A device that can't install the new build loses the wallet's screens for good, including the recovery phrase.
- Try it on
previewfirst. To undo, set the earlier numbers again, or 0. - Only builds that already have the check can be blocked, whether it came in the store build or over the air. Builds launch blocked offline too: the last answer is kept on the device.
- The policy is unsigned. Whoever takes over the server can block builds below a number, but can't
send anyone elsewhere: the store links are built into the app (
constants/links.ts). - Development builds and Detox runs skip the check. Release builds made outside EAS report build 1, so any production minimum blocks them.
- It runs with
noderather than as an npm script on purpose:package.jsonscripts are part of the runtime version, so adding one would cut installed builds off from new updates.