I had just added a new settings screen to one of my wallpaper apps. I sent it to my iPhone through Rork Companion, touched it, scrolled it, and everything landed where I expected. I said "done" out loud that night.
The gap showed up a week later. In the build I assembled for distribution, the same screen looked identical down to the pixel, but the purchase flow gave me nothing back. It had been running on a real device, that much was true. What had been running was a development build, not the build a user would actually receive.
I want to say this early: none of that is a shortcoming in Companion. Companion did its job. I was the one who counted a narrow check as a wide one.
Three steps to get it on your phone, and the third one is where people stop
The sequence itself is short.
- Open Rork's install page in Safari on your iPhone
- Install Companion from the link or QR code shown there
- Go to Settings, then General, then VPN & Device Management, and trust the developer profile
Almost everyone gets stuck on the third step. "Untrusted Developer" appears and the app refuses to launch. What helps here is a reframe: that message is not a failure, it is iOS doing its job correctly. Anything that arrives outside the store is not allowed to run until the person holding the phone says so explicitly.
There is a second thing worth knowing. A development profile signed with a free Apple ID has a short life, usually a few days to about a week. An app that opened yesterday and refuses to open this morning is not broken; its signature simply expired. Installing it again the same way brings it back.
I have come to treat that reinstall as a small inventory check rather than a chore. Each time, I have to say out loud what I am actually trying to verify.
What Companion does show you
Putting an app on a real device surfaces more than I used to assume.
| What you can see | Why the browser hides it |
|---|---|
| Real touch targets | A mouse pointer is a point; a fingertip is an area |
| Safe areas and notches | Per-device measurements do not survive a resized frame |
| Effect of the system font size | Plenty of people run their phone with larger text |
| Inputs hidden by the keyboard | Real keyboard height and its animation delay both matter |
| Latency on a real connection | Your own fast connection hides the slow-network experience |
Clearing these in Companion makes everything downstream easier. Most of my layout adjustments never leave this stage.
What Companion cannot answer
Some questions stay open no matter how well the app behaves in Companion. The purchase flow I missed lives in this list.
| What stays unverified | Where to verify it |
|---|---|
| Real purchases and subscription renewals | An internal testing track or a TestFlight build |
| Speed and memory in a release build | Bundling and optimization differ, so measure on the distributed build |
| Review-facing copy, screenshots, privacy entries | The submission forms and the artifacts you actually upload |
| The update channel used for OTA delivery | A build assembled with that channel set |
| Extension targets such as widgets | A distributed build signed together with the extension |
Push notifications deserve a line of their own. A development build talks to a different delivery environment than a store build, so a notification that arrives perfectly during testing tells you very little about the one your users will receive. I now treat "a notification arrived" and "a notification arrived through the production path" as two separate checkmarks on two separate days.
Written down like this it reads as obvious. In the middle of the work, though, the boundary dissolves. When something is moving in your hand, suspecting that it might not be is genuinely hard.
"Does it run" is a question Companion can answer. "Can I ship it" is a question only a distribution build can answer. Since I put that line at the top of my working notes, I have not repeated the same miss.
Put the current build on the screen
If the boundary dissolves, I decided to mark it. I keep a small component that prints build information in the corner, but only during development and internal distribution.
// components/BuildBadge.tsx
import { Text, View } from 'react-native';
import Constants from 'expo-constants';
import * as Application from 'expo-application';
import * as Updates from 'expo-updates';
export function BuildBadge() {
const forced = Constants.expoConfig?.extra?.showBuildBadge === true;
if (!__DEV__ && !forced) return null;
const lines = [
`env: ${Constants.executionEnvironment}`,
`id: ${Application.applicationId ?? '-'}`,
`ver: ${Application.nativeApplicationVersion ?? '-'} (${Application.nativeBuildVersion ?? '-'})`,
`channel: ${Updates.channel ?? '(none)'}`,
`embedded: ${String(Updates.isEmbeddedLaunch)}`,
];
return (
<View
pointerEvents="none"
style={{ position: 'absolute', right: 8, bottom: 8, padding: 6, borderRadius: 6, backgroundColor: 'rgba(0,0,0,0.6)' }}
>
{lines.map((line) => (
<Text key={line} style={{ color: '#fff', fontSize: 10 }}>{line}</Text>
))}
</View>
);
}A quick reading guide. Constants.executionEnvironment tells you whether you are running through a store-distributed client or a build you assembled yourself. Updates.channel shows which channel the build was configured to receive updates from, and it stays empty during development. When Updates.isEmbeddedLaunch is true, you are running the code bundled into the binary and have not yet received an OTA update.
I keep the switch itself in configuration.
// app.config.ts
export default ({ config }) => ({
...config,
extra: {
...config.extra,
showBuildBadge: process.env.SHOW_BUILD_BADGE === '1',
},
});With that in place, passing SHOW_BUILD_BADGE=1 while assembling an internal build shows the badge, and the production build drops it without me remembering to.
The badge earns its keep in one more place: bug reports. I ask testers to include that corner text in whatever they send me — a screenshot is enough, since the badge is already in the frame. Before that, a report would say "the purchase button did nothing" and I would spend an evening reproducing it against the wrong build. Now the first line of every report answers that question for me.
For a while I gated this on __DEV__ alone. The badge then disappeared in internal builds, and bug reports from testers stopped telling me which build they were describing. Splitting the condition into two axes came out of that specific frustration, not out of a design principle I read somewhere.
The order I follow now
These days I move through three stages. The point of the stages is as much about what I refuse to look at as what I check.
| Stage | What I check here | What I deliberately skip here |
|---|---|---|
| Companion | Layout, feel, latency on a real connection | Purchases, production push delivery, launch speed |
| Internal distribution | Purchase flow, update channel, extension targets | Fine spacing adjustments |
| Staged rollout | Stability in real users' environments | Adding new features |
What surprised me is how much the third column changed my pace. Deciding in advance not to look at spacing during internal distribution sounds like carelessness, and for a while it felt that way. In practice it stopped me from re-opening a screen I had already settled, and it kept the internal round short enough that testers were still willing to reply. A stage that tries to check everything ends up checking nothing carefully.
Separating the stages narrows the search space when something breaks. For how I handle releases after that point, I wrote up the details in staged rollout and hotfix strategy. If the build itself is not completing yet, the build failure checklist will get you further, faster.
If you are weighing how far the free tier takes you before paying for a plan, my hands-on comparison of the pricing plans may give you something concrete to decide with.
One thing to do today
If you have an app running through Companion right now, start by adding the build badge alone. It takes about five minutes, and from then on the corner of your screen answers the question "which build am I actually looking at" without you having to guess.
I spent a long time guessing before I did this. Thank you for reading — I hope it saves you the detour it cost me.