<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Attay Rasool</title>
    <description>The latest articles on DEV Community by Attay Rasool (@attayr).</description>
    <link>https://dev.to/attayr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4163876%2F3264720a-aa1c-4006-8ee3-825c21b3e4a8.jpg</url>
      <title>DEV Community: Attay Rasool</title>
      <link>https://dev.to/attayr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/attayr"/>
    <language>en</language>
    <item>
      <title>I scanned 23 open-source Expo apps before they hit the App Store. Here's what a pre-flight check caught.</title>
      <dc:creator>Attay Rasool</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:18:49 +0000</pubDate>
      <link>https://dev.to/attayr/i-scanned-23-open-source-expo-apps-before-they-hit-the-app-store-heres-what-a-pre-flight-check-fb6</link>
      <guid>https://dev.to/attayr/i-scanned-23-open-source-expo-apps-before-they-hit-the-app-store-heres-what-a-pre-flight-check-fb6</guid>
      <description>&lt;p&gt;Every Expo team has the same Friday afternoon: you run &lt;code&gt;eas build&lt;/code&gt;, wait twenty minutes, and the build fails on something that was knowable before you started. Or worse, the build succeeds, the store review rejects it three days later, and the reason was a missing line in &lt;code&gt;app.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;expo-doctor&lt;/code&gt; checks your dependencies. Nothing I could find checks the things that actually get builds and reviews rejected: usage strings, permissions, &lt;code&gt;eas.json&lt;/code&gt;, update channels, committed secrets. So I built a small CLI to do it, and then pointed it at real apps to see whether the problems are real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo-preflight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads &lt;code&gt;app.json&lt;/code&gt; / &lt;code&gt;app.config.*&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;eas.json&lt;/code&gt; and &lt;code&gt;.gitignore&lt;/code&gt;, never runs your app and never prints secret values. It's MIT licensed: &lt;a href="https://github.com/AttayR/expo-preflight" rel="noopener noreferrer"&gt;https://github.com/AttayR/expo-preflight&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;I ran it, read-only, against 23 apps from 19 popular open-source repos (boilerplates, Expo's own examples, and real apps with thousands of stars). I cloned them, ran the tool, and did not open issues or PRs on any of them. No names below: these are mostly well-maintained projects and the point is the pattern, not the people.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong first
&lt;/h2&gt;

&lt;p&gt;My first run produced 36 errors and 56 warnings, and roughly half were wrong. I read the findings against the real configs and found the tool was noisy in predictable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It flagged the standard React Native &lt;code&gt;debug.keystore&lt;/code&gt; as a leaked secret.&lt;/li&gt;
&lt;li&gt;It didn't understand EAS server-side environments or &lt;code&gt;process.env.X ?? "default"&lt;/code&gt;, so it reported env vars as undefined.&lt;/li&gt;
&lt;li&gt;It only read the app folder's &lt;code&gt;.gitignore&lt;/code&gt;, so monorepos looked like they ignored nothing.&lt;/li&gt;
&lt;li&gt;Its Android permission table was missing things like &lt;code&gt;WRITE_CALENDAR&lt;/code&gt; for &lt;code&gt;expo-calendar&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the unglamorous lesson: a linter you can't trust gets ignored. I fixed all of these and wrote regression tests for each (72 tests now), and the error+warning count on the same 23 apps dropped from 83 to 29 (7 errors and 22 warnings on the published 0.1.2; 13 of the 23 apps still had at least one finding). The findings that survived are the ones below, and I'll be upfront that it is still not perfect: I spot-checked six of them against the real code, and four were right and two were wrong (see "Limits").&lt;/p&gt;

&lt;h2&gt;
  
  
  What was real
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Missing iOS usage strings (4 of the 6 apps with &lt;code&gt;expo-image-picker&lt;/code&gt; or &lt;code&gt;expo-media-library&lt;/code&gt; installed, on both 0.1.1 and the stricter 0.1.2).&lt;/strong&gt;&lt;br&gt;
Apps using &lt;code&gt;expo-image-picker&lt;/code&gt; or &lt;code&gt;expo-media-library&lt;/code&gt; without the matching &lt;code&gt;NSPhotoLibraryUsageDescription&lt;/code&gt;, &lt;code&gt;NSCameraUsageDescription&lt;/code&gt; or &lt;code&gt;NSPhotoLibraryAddUsageDescription&lt;/code&gt;. Without them iOS terminates the app when the picker opens, and review can reject it. This is the classic one because it's invisible in development builds that use Expo Go's own strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A tracked &lt;code&gt;.env&lt;/code&gt; in git (2 of 23 apps).&lt;/strong&gt;&lt;br&gt;
Two apps commit a &lt;code&gt;.env&lt;/code&gt;. In one it holds only a public Sentry DSN, which is low risk, but &lt;code&gt;.gitignore&lt;/code&gt; didn't ignore &lt;code&gt;.env&lt;/code&gt;, so the next real secret goes in the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Update channels that don't exist (2 of the 6 apps with both &lt;code&gt;expo-updates&lt;/code&gt; and an &lt;code&gt;eas.json&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
One app had &lt;code&gt;expo-updates&lt;/code&gt; installed, but its &lt;code&gt;preview&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; build profiles never set a &lt;code&gt;channel&lt;/code&gt;. Those builds can never receive an EAS Update. Nothing fails; the OTA pipeline just silently does nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;cli.appVersionSource&lt;/code&gt; unset (4 of the 13 apps with an &lt;code&gt;eas.json&lt;/code&gt;).&lt;/strong&gt;&lt;br&gt;
Four apps leave it unset, which leaves it ambiguous where the app version comes from (EAS recommends setting it explicitly).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Play-restricted permissions (1 of 23).&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;REQUEST_INSTALL_PACKAGES&lt;/code&gt; declared in &lt;code&gt;app.json&lt;/code&gt; needs a Play Console declaration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this tells me
&lt;/h2&gt;

&lt;p&gt;Almost none of this is exotic. It's configuration that's correct in each file and wrong across files: a plugin in &lt;code&gt;app.json&lt;/code&gt; that doesn't match a package in &lt;code&gt;package.json&lt;/code&gt;, a profile in &lt;code&gt;eas.json&lt;/code&gt; that doesn't match an &lt;code&gt;expo-updates&lt;/code&gt; install. Humans are bad at cross-file consistency and machines are good at it, which is the entire argument for a pre-flight check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo-preflight            &lt;span class="c"&gt;# pretty output&lt;/span&gt;
npx expo-preflight &lt;span class="nt"&gt;--format&lt;/span&gt; github   &lt;span class="c"&gt;# Markdown for a PR comment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--format github&lt;/code&gt; prints the report as Markdown, ready to paste into a PR comment. Rules can be toggled or re-leveled in &lt;code&gt;.preflightrc.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's static analysis. For &lt;code&gt;app.config.js/ts&lt;/code&gt; it extracts values best-effort rather than evaluating them, and says so.&lt;/li&gt;
&lt;li&gt;"Package never imported" detection is a regex over source files, not a full analysis.&lt;/li&gt;
&lt;li&gt;Precision wasn't where I wanted it. In a six-finding spot check of the published 0.1.1, 4 were correct. Both wrong ones were iOS usage-string errors raised because a package is &lt;em&gt;installed&lt;/em&gt;, not because the sensitive API is &lt;em&gt;called&lt;/em&gt; (an app using only the accelerometer doesn't need a motion string; one that only opens the photo library never needs the camera string). Version 0.1.2 fixes this for the packages where I mapped the triggering APIs (image picker, camera, media library, location, sensors): it now errors only when the API is actually called, and downgrades the rest to info. Other packages (contacts, local authentication, BLE and so on) are still flagged just for being installed; mapping those is next. Those two findings flipped to info on re-scan, and none of the real ones disappeared. That's still only six spot-checks, not a benchmark.&lt;/li&gt;
&lt;li&gt;23 apps is a small sample, and six spot-checks is smaller. Treat the proportions as indicative, not statistics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it flags something wrong on your project, please open an issue with the (redacted) config. That's how 0.1.1 and 0.1.2 got better, by running the tool on real code, and I'd rather fix a false positive than have it erode trust.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AttayR/expo-preflight" rel="noopener noreferrer"&gt;https://github.com/AttayR/expo-preflight&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>expo</category>
    </item>
  </channel>
</rss>
