Deployment
hosting, Android, iOS, macOS.
Put the admin panel and the web wallet online, and build signed Android and iOS releases for Google Play, the App Store and TestFlight.
The commands on this page are written for macOS and Linux shells. On Windows, run them in Git Bash or WSL, where they work as written. In PowerShell, set a variable with $env:NAME="value" instead of export. Flutter, Node.js and the Firebase and FlutterFire CLIs run natively on Windows; iOS and macOS builds need a Mac.
01Before any release
- Rebrand done (checklist).
crypto_wallet/.envfilled with your keys, includingADMIN_CONFIG_URLif you use it.flutterfire configurerun in both apps for your production Firebase project.- Version raised in
crypto_wallet/pubspec.yaml:version: 1.0.0+9means version 1.0.0, build 9. Every store upload needs a higher build number. - Checks pass:
cd crypto_wallet
flutter analyze
flutter test
02Admin panel on Firebase Hosting
crypto_wallet_admin/firebase.json has a hosting target admin. Its pre-deploy step builds the panel with PRODUCTION_MODE=true and the demo login off, and sends /config.json to the getRuntimeConfigJson function.
cd crypto_wallet_admin
firebase hosting:sites:create <admin-site>
firebase target:apply hosting admin <admin-site>
firebase deploy --only hosting:admin
- Add
<admin-site>.web.app(and any custom domain) to Authentication → Settings → Authorized domains, or sign-in fails. - Set
ADMIN_CONFIG_URL=https://<admin-site>.web.app/config.jsonincrypto_wallet/.envand rebuild the app (how). - Custom domain: Hosting → your site → Add custom domain, add the DNS records at your registrar, wait for the certificate.
Demo mode (sample data, a local demo login) exists only when the panel is built with the demo build switch (--dart-define=ADMIN_CLIENT_DEMO_LOGIN_ENABLED=true). The package contains no demo email or password: if you ever build a demo, you choose them yourself with ADMIN_CLIENT_DEMO_EMAIL and ADMIN_CLIENT_DEMO_PASSWORD. Use such a build only on a separate site, never as your real admin.
Optional demo admin. firebase.json has a second hosting target, admin-demo: the same panel with a browser-local demo login that never writes to Firestore.
It is off unless you build it with --dart-define=ADMIN_CLIENT_DEMO_LOGIN_ENABLED=true plus --dart-define=ADMIN_CLIENT_DEMO_EMAIL=… and --dart-define=ADMIN_CLIENT_DEMO_PASSWORD=… (12+ characters), then firebase deploy --only hosting:admin-demo.
ADMIN_DEMO_MODE=true separately enables the offline preview workspace. No demo credentials are stored in the package.
03Web wallet on Firebase Hosting
cd crypto_wallet
flutter build web --release --dart-define-from-file=.env
firebase hosting:sites:create <wallet-site>
firebase target:apply hosting app <wallet-site>
firebase deploy --only hosting:app
crypto_wallet/firebase.json already has the app target with single-page routing, caching and security headers. Add the domain to Authorized domains. For web push, fill the firebaseConfig block in web/firebase-messaging-sw.js and add --dart-define=WEB_PUSH_VAPID_KEY=<key> to the build.
04Web wallet or admin on Vercel
Both builds are static sites and run on any HTTPS host. With Vercel, build first, add a vercel.json to the build folder, then deploy that folder. flutter build web empties build/web, so copy vercel.json in after every build.
Wallet
cd crypto_wallet
flutter build web --release --dart-define-from-file=.env
cat > build/web/vercel.json <<'EOF'
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
EOF
npx vercel deploy build/web --prod
Admin panel
The admin must be built with the production flags, and /config.json has to reach your function:
cd crypto_wallet_admin
flutter build web --release --dart-define=PRODUCTION_MODE=true --dart-define=ADMIN_CLIENT_DEMO_LOGIN_ENABLED=false
cat > build/web/vercel.json <<'EOF'
{
"rewrites": [
{ "source": "/config.json",
"destination": "https://europe-west1-<your-project-id>.cloudfunctions.net/getRuntimeConfigJson" },
{ "source": "/(.*)", "destination": "/index.html" }
]
}
EOF
npx vercel deploy build/web --prod
Add the Vercel domain to Firebase Authorized domains. The security headers in firebase.json (frame, content-type, referrer and permissions policies, CSP) do not travel automatically; set them in Vercel under headers if you want the same protection.
Copy the policy from firebase.json instead of writing a looser one: it allows no 'unsafe-inline' or 'unsafe-eval' at all and names its script origins (script-src 'self' 'wasm-unsafe-eval' https://www.gstatic.com), with connect-src 'self' https: wss: for RPC endpoints and the WalletConnect relay.
05Content-Security-Policy and Flutter upgrades
Both firebase.json files send a policy with no 'unsafe-inline' in script-src or style-src. The page's own styles are in web/splash.css (wallet) and web/styles.css (admin), and its scripts are files too. The Flutter engine adds two <style> blocks of its own at start-up; style-src allows exactly those two by hash:
style-src 'self' 'sha256-2hxujCU6WzSO6ycZznxFKlHE++BDsDuMYXT/O0zl8rM='
'sha256-cgINIZw4iKgU7zccO2vPeZdQJIMlKEchLZG6hpkP+8k=' https://fonts.googleapis.com
The hashes match the engine of Flutter 3.41. After you upgrade Flutter, open the web build with the browser console visible. If it reports a refused inline style, it prints the hash it expected ('sha256-…'): put that hash in place of the old one in style-src, in both apps. Do not add 'unsafe-inline' instead.
The console also lists inline scripts the policy refused: FlutterFire tries to inject them, and web/firebase_csp_loader.js loads the same Firebase SDKs from https://www.gstatic.com/firebasejs/ instead, so those messages are expected.
The wallet's script-src also names https://www.google.com/recaptcha/, which App Check's reCAPTCHA v3 provider needs on the web (App Check).
06Backend updates
cd crypto_wallet
firebase deploy --only firestore:rules,firestore:indexes # rules or indexes changed
firebase deploy --only functions # functions or functions/.env changed
firebase deploy --only functions:recordFeeEvent # one function
07Android: APK and App Bundle
- Create an upload keystore onceBack up the file and the passwords; without them you cannot update your app.
keytool -genkey -v -keystore ~/upload-keystore.jks \ -keyalg RSA -keysize 2048 -validity 10000 -alias upload - Create android/key.propertiesNext to
android/build.gradle.kts. Never commit it. On a CI server you can useANDROID_KEYSTORE_PATH,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIAS,ANDROID_KEY_PASSWORDinstead.storePassword=<your store password> keyPassword=<your key password> keyAlias=upload storeFile=/path/to/upload-keystore.jks - Build
cd crypto_wallet # App Bundle for Google Play flutter build appbundle --release --dart-define-from-file=.env # APK to install on a test phone flutter build apk --release --dart-define-from-file=.env - UploadCreate a release in Play Console and upload
build/app/outputs/bundle/release/app-release.aab, as described in Google's guide Prepare and roll out a release. Use Play App Signing.
Without signing values the release build falls back to the debug key so that local release runs work. Google Play rejects debug-signed bundles.
08iOS: App Store and TestFlight
- Install pods
cd crypto_wallet/ios pod install --repo-update - SigningOpen
ios/Runner.xcworkspacein Xcode → Runner → Signing & Capabilities. Select your Team, check the bundle ID, keep Push Notifications, In-App Purchase and Keychain Sharing. - Push keyCreate an APNs authentication key in your Apple Developer account and upload it to your Firebase project, as described in Firebase's guide Configure APNs with FCM.
- Build the archive
cd crypto_wallet flutter build ipa --release --dart-define-from-file=.env - Upload to TestFlightXcode → Window → Organizer → Distribute App → App Store Connect, or drag the
.ipafrombuild/ios/ipa/into Apple's Transporter app. After processing it appears under TestFlight; add testers there. - Submit for reviewRewrite the permission texts in
ios/Runner/Info.plist(camera, Face ID, local network) with your app's name — reviewers read them.
09macOS: Mac App Store or your own download
The same source builds both. The only difference is the crypto PRO payment, which store policy does not allow inside an App Store app:
| Build | Command | Crypto PRO payment |
|---|---|---|
| Mac App Store | flutter build macos --release --dart-define-from-file=.env | Hidden |
| Your own download (for example a notarized DMG) | flutter build macos --release --dart-define-from-file=.env --dart-define=DIRECT_DISTRIBUTION=true | Shown when enabled in Admin → System Config |
DIRECT_DISTRIBUTION is false unless you pass it. Never pass it for a build you upload to App Store Connect.
10Store review notes
- Apple 3.1.5: crypto wallets must come from a developer enrolled as an organisation.
- Apple 3.1.1: sell PRO with In-App Purchase inside the iOS app; do not link to outside payment on iOS.
- Google Play: some countries require wallet providers to be registered or licensed. Check the policy pages for the countries you publish in.
- Fill App Privacy (Apple) and Data safety (Google) truthfully; Firebase Analytics, Crashlytics and Messaging collect data.
- Tell reviewers the wallet is non-custodial, how to create a test wallet, and give them PRO (Admin → Users → Grant PRO).
These are practical pointers. You are responsible for the licences and disclosures your business needs.
11After deploying: is it live?
- The admin URL shows the Sign In screen, with no "Preview" or "Client demo" badge.
/config.jsonon your admin returns JSON with your values.- A change saved and published in the admin appears in the app after a restart.
- A test push from Admin → Notifications arrives on a test phone.
- The function logs show no repeated errors (viewing logs, or
firebase functions:log).