User Testing — TestFlight & Android
This guide covers getting the RL Padel Academy app onto real devices for testing, on both iOS (via TestFlight) and Android (via Google Play internal testing). We use EAS Build to compile signed binaries in the cloud and EAS Submit to upload them to the stores.
The app runs Expo SDK 56 with the new architecture enabled, bundle ID nl.rlpadelacademy.app. Because parts are native, distributing to testers requires a real signed build — you can’t just send a JS bundle.
Overview
The flow is the same on both platforms:
eas buildproduces a signed binary in Expo’s cloud (no local Xcode/Android Studio toolchain needed).eas submituploads that binary to App Store Connect or Google Play.- You add testers to a track/group and they install.
- Between native builds, ship JS-only fixes instantly with
eas update.
Install the CLI and log in once:
npm install -g eas-clieas logincd /Users/stijn/Developer/wva-shb-partnership/padel/apps/frontendeas.json
Build profiles live in apps/frontend/eas.json. This defines three environments, each pointed at the appropriate PocketBase backend:
{ "cli": { "version": ">= 12.0.0", "appVersionSource": "remote" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "ios": { "simulator": true }, "env": { "EXPO_PUBLIC_PB_URL": "http://localhost:8090" } }, "preview": { "distribution": "internal", "channel": "preview", "ios": { "resourceClass": "m-medium" }, "android": { "buildType": "apk" }, "env": { "EXPO_PUBLIC_PB_URL": "https://staging-pb.rlpadelacademy.nl" } }, "production": { "channel": "production", "autoIncrement": true, "android": { "buildType": "app-bundle" }, "env": { "EXPO_PUBLIC_PB_URL": "https://rl-padel-pb.fly.dev" } } }, "submit": { "production": { "ios": { "appleId": "stijn@stijnbakker.com", "ascAppId": "0000000000", "appleTeamId": "XXXXXXXXXX" }, "android": { "track": "internal", "serviceAccountKeyPath": "./google-service-account.json" } } }}- development builds a dev client for the iOS simulator and points at a local PocketBase.
- preview builds an internally-distributable binary (APK on Android, ad-hoc IPA / TestFlight on iOS) against a staging backend.
- production builds a store-ready AAB / IPA against the live Fly.io backend and auto-increments the build number.
iOS / TestFlight
Requirements
- An Apple Developer Program membership ($99/year). This is non-negotiable for any device distribution beyond the simulator.
- The app registered in App Store Connect with bundle ID
nl.rlpadelacademy.app.
Build and submit
# Build a signed IPA in the cloud (EAS manages certificates & provisioning)eas build --platform ios --profile production
# Upload the resulting build to App Store Connect / TestFlighteas submit --platform ios --profile production --latestOn first run, EAS offers to create the App Store Connect app record and manage your signing credentials — accept this unless you have an existing setup.
TestFlight groups
In App Store Connect, open TestFlight. There are two kinds of testers:
- Internal testing — up to 100 users who are members of your App Store Connect team. Builds are available almost immediately with no App Review. Use this for yourself and close collaborators.
- External testing — up to 10,000 testers invited by email or public link. The first build submitted to an external group requires a short Beta App Review by Apple (usually a few hours to a day). Subsequent builds of the same version are auto-approved.
Create a group, add tester emails (or generate a public link), and assign the build to the group.
How testers install
- Tester installs TestFlight from the App Store.
- They open the email invite (or public link) and tap View in TestFlight / Accept.
- They tap Install, then launch the app from TestFlight.
Testers can leave feedback and attach screenshots directly from TestFlight (take a screenshot while testing, then Share Beta Feedback) — this lands in App Store Connect with device and OS metadata attached.
Android
Android has a lower barrier: a $25 one-time Google Play registration fee, and you can also just share an APK directly for a quick test.
Build
# Quick share: produces an installable APK (sideload / direct download)eas build --platform android --profile preview
# Store-ready bundle for Google Playeas build --platform android --profile productionThe preview profile sets "buildType": "apk", giving you a single file you can send to a tester via the EAS build page link. They enable “install from unknown sources” and install it — great for fast iteration without the Play Console.
Google Play Internal Testing
For a more realistic path (and to test Play-managed signing and updates):
- Create the app in the Google Play Console under bundle ID
nl.rlpadelacademy.app. - Create a service account key and save it as
apps/frontend/google-service-account.json(referenced ineas.json). - Submit to the Internal testing track:
eas submit --platform android --profile production --latest- In the Play Console, open Testing → Internal testing, create a tester list (email addresses), and share the opt-in link. The internal track has no review delay and supports up to 100 testers.
Pointing builds at the right backend
The app reads EXPO_PUBLIC_PB_URL at build time. Each eas.json profile sets it explicitly:
development→http://localhost:8090(your machine).preview→ a staging PocketBase instance.production→ the live Fly.io backend.
Use a separate staging PocketBase for testers so they can create accounts, bookings, and other data without polluting production — and so a buggy test build can’t corrupt real members’ data. Spin one up exactly like production (see the Fly.io deployment guide) under a different app name and volume.
OTA updates between builds
For JS / asset-only changes (UI tweaks, copy fixes, logic that doesn’t touch native code), you don’t need a new store build. Push an over-the-air update to the matching channel:
# Ship a JS fix to everyone on the preview buildeas update --branch preview --message "Fix booking confirmation copy"
# ...or to production testerseas update --branch production --message "Adjust schedule layout"Testers pick up the update the next time they open the app. Note: anything that changes native code, the SDK version, or app.json native config still requires a fresh eas build.
A pragmatic test plan
Recruit roughly 8 club members spread across the app’s roles so every permission path gets exercised:
- 4–5 lid (regular members) — booking, schedule, profile.
- 2 trainer — managing/viewing their sessions.
- 1–2 eigenaar (owner/admin) — club management, member overview.
Give each tester a short feedback checklist:
- Onboarding: can you register and log in?
- Booking: reserve a slot, see it on the schedule, cancel it.
- Notifications/reminders behave as expected.
- Anything slow, confusing, visually broken, or crashing?
- Does the app survive backgrounding and a cold restart?
Ask iOS testers to use TestFlight’s built-in screenshot + feedback flow so reports arrive with device/OS context. Collect Android feedback in a shared doc or your issue tracker.
Versioning reminder
The app is on SDK 56 with the new architecture. Bump the build number on every submission — iOS buildNumber and Android versionCode must be strictly higher than any previously uploaded build or the store will reject it. The production profile above sets "autoIncrement": true with "appVersionSource": "remote", so EAS manages these for you automatically. If you ever build manually, increment them yourself in app.json.