<?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: Piyush</title>
    <description>The latest articles on DEV Community by Piyush (@titan00001).</description>
    <link>https://dev.to/titan00001</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%2F870639%2F97a55d45-68a8-43b9-98bb-d4740e7b857a.png</url>
      <title>DEV Community: Piyush</title>
      <link>https://dev.to/titan00001</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/titan00001"/>
    <language>en</language>
    <item>
      <title>Splitting the Monolith: What We Learned Separating Workflows from the Main Server</title>
      <dc:creator>Piyush</dc:creator>
      <pubDate>Thu, 16 Jul 2026 17:17:49 +0000</pubDate>
      <link>https://dev.to/titan00001/splitting-the-monolith-what-we-learned-separating-workflows-from-the-main-server-41i5</link>
      <guid>https://dev.to/titan00001/splitting-the-monolith-what-we-learned-separating-workflows-from-the-main-server-41i5</guid>
      <description>&lt;p&gt;&lt;em&gt;A story about scaling two very different workloads independently — and the one thing that broke when we did.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;For a long time, our application ran as a single process. One Node.js server handled everything: HTTP requests, WebSocket connections to browsers, and the workflow engine that lets a process pause and resume later — sending a message, waiting, sending a reminder, and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F887y1nqrfq8udk24s826.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F887y1nqrfq8udk24s826.png" alt=" " width="509" height="764"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That worked fine while traffic was small. But two things about that workload didn't sit well together as we grew.&lt;/p&gt;

&lt;p&gt;User-facing traffic is bursty and latency-sensitive — someone opens the app, sends a message, expects the interface to respond immediately. Workflow execution is background work with a completely different profile: timers firing minutes or hours later, jobs that need to survive a restart, execution that has nothing to do with any browser currently connected. Bundling both into one process meant we couldn't scale them independently. A spike in workflow volume competed for the same CPU and memory as someone waiting on an API response. Scaling up meant scaling everything, whether it needed it or not.&lt;/p&gt;

&lt;p&gt;So we made the call to split them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The split
&lt;/h2&gt;

&lt;p&gt;We pulled workflow execution into its own process — a dedicated worker, separate from the API, talking to Redis for delayed jobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffx36g9r5nnfaals09pny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffx36g9r5nnfaals09pny.png" alt=" " width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the two workloads could scale on their own terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;API&lt;/strong&gt; scales with concurrent users and request volume.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;worker&lt;/strong&gt; scales with the number of active, in-flight workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both processes shared the same database, so as long as each did its job correctly, the system as a whole should behave exactly as it did before — just with each half able to grow at its own pace.&lt;/p&gt;

&lt;p&gt;That was the theory. Deploying it surfaced something the all-in-one version had been hiding from us for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the split quietly broke
&lt;/h2&gt;

&lt;p&gt;Once workflows ran in their own process, timers still fired exactly when they should. The workflow still resumed. The next chat message still landed in the database, correctly, every time.&lt;/p&gt;

&lt;p&gt;But users stopped seeing it. Refreshing the page always showed the message — proving the data was right — yet nothing told the browser to update on its own.&lt;/p&gt;

&lt;p&gt;Chasing this, I found the actual cause: the worker had a &lt;code&gt;ChatEventsService&lt;/code&gt;, and so did the API — the exact same code, copied over as part of the split. In the old, single-process world, that class used one shared &lt;code&gt;EventEmitter&lt;/code&gt;, and any part of the app could react to what any other part had just done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuigjei66vhu2s4ebko1u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuigjei66vhu2s4ebko1u.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the split, that assumption quietly stopped being true. Two processes were now running identical code, importing the same service, calling the same method name — but each with its own &lt;code&gt;EventEmitter&lt;/code&gt;, with no memory shared between them at all. The worker was still emitting &lt;code&gt;message.created&lt;/code&gt;. It just had nobody left in its own process who needed to hear it, and no way to reach the process that did.&lt;/p&gt;

&lt;p&gt;This is the cost that's easy to miss when you split a monolith for scaling reasons: you're not just separating CPU load, you're separating &lt;strong&gt;memory&lt;/strong&gt;. Anything that relied on being in the same process — including things you didn't realize relied on it — stops working silently, not loudly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reframing the question
&lt;/h2&gt;

&lt;p&gt;My first instinct was to debug it as a socket problem — was Socket.IO dropping connections, was the frontend failing to re-render. All of that was fine. The real question wasn't about sockets at all:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How does the knowledge that a message was created travel from the worker to the API, now that they don't share memory?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before the split, the answer was "it doesn't have to travel — it's already there." That answer had been invisible because it had never needed to be an answer. Splitting the process for scaling reasons was exactly what turned an implicit assumption into a missing piece of infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Giving the two processes a way to talk
&lt;/h2&gt;

&lt;p&gt;Once the split was scaling both workloads the way we wanted, the remaining gap wasn't really about this one worker and this one browser — it was that we now had two independent processes with no explicit channel between them, for anything.&lt;/p&gt;

&lt;p&gt;A few options would have patched just this one case:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Why it undoes the split&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Move timer execution back into the API&lt;/td&gt;
&lt;td&gt;Reintroduces the exact resource contention we split to avoid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add Socket.IO to the worker&lt;/td&gt;
&lt;td&gt;Worker takes on browser-communication it shouldn't own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll for updates from the browser&lt;/td&gt;
&lt;td&gt;Extra load, worse UX, and doesn't scale either&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of those either walked the split back or bolted on a one-off fix for this specific path. What we actually needed was a general channel: any process drops a message onto a queue, any other process that cares picks it up — without the two ever needing to share memory again. We introduced a &lt;strong&gt;message queue&lt;/strong&gt; between them, backed by Redis, since we were already depending on Redis for timers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn2y14kqrgs1tatpodyak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn2y14kqrgs1tatpodyak.png" alt=" " width="799" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ownership stayed exactly where the split put it. The worker still owns workflow execution. The API still owns WebSockets and browsers. We added one explicit path for a fact to cross the boundary we'd just created: the worker drops a message on the queue, the API consumes it, nothing more.&lt;/p&gt;




&lt;h2&gt;
  
  
  What independent scaling actually cost us
&lt;/h2&gt;

&lt;p&gt;Splitting the monolith gave us what we wanted: the API and the worker now scale on completely different axes, and a burst of workflow activity no longer competes with user-facing request latency. But it wasn't free, and it's worth being honest about the trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We gave up &lt;strong&gt;implicit communication&lt;/strong&gt;. Anything that used to "just work" because it lived in shared memory now has to be made explicit, on purpose, for every new interaction between the two processes.&lt;/li&gt;
&lt;li&gt;We gained a &lt;strong&gt;new failure mode&lt;/strong&gt;. A message queue is only as reliable as we configure it to be — if we're not careful about acknowledgments and persistence, a message can be dropped if the API is mid-deploy or briefly disconnected at the exact moment something is enqueued.&lt;/li&gt;
&lt;li&gt;We took on a &lt;strong&gt;new category of problems&lt;/strong&gt;: making sure messages are delivered reliably, consumed exactly once, processed in the right order, and observable when something goes wrong. None of those existed when everything ran in one process — they're the direct cost of scaling the two halves independently, and they deserve their own write-up rather than a rushed paragraph here.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Splitting a monolith to scale two workloads independently is usually described as an infrastructure decision — more instances of the thing that needs to scale, fewer of the thing that doesn't. That part's true. What's less obvious going in is that the split also quietly removes something you were never billed for: a shared memory space that had been doing communication work on your behalf, for free, the whole time.&lt;/p&gt;

&lt;p&gt;I had been assuming that persisting a change and communicating a change were the same operation because, inside a monolith, they often feel that way. Splitting the system forced me to see they're completely different problems. A database answers "What is true?" A messaging system answers "Who needs to know that it became true?" Once I started reasoning with those as separate concerns, the architecture became much easier to understand.&lt;/p&gt;

&lt;p&gt;(Refined with AI for readability)&lt;/p&gt;

</description>
      <category>pubsub</category>
      <category>webdev</category>
      <category>websocket</category>
      <category>learning</category>
    </item>
    <item>
      <title>"pod install" Fixed My Expo Package Version (And Taught Me the Difference Between "npm install" and "npx expo install")</title>
      <dc:creator>Piyush</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:21:16 +0000</pubDate>
      <link>https://dev.to/titan00001/pod-install-fixed-my-expo-package-version-and-taught-me-the-difference-between-npm-install-and-11cb</link>
      <guid>https://dev.to/titan00001/pod-install-fixed-my-expo-package-version-and-taught-me-the-difference-between-npm-install-and-11cb</guid>
      <description>&lt;h2&gt;
  
  
  The Problem That Didn't Make Sense
&lt;/h2&gt;

&lt;p&gt;I was working on an Expo-managed React Native application targeting iOS. Everything looked normal until I noticed a version mismatch involving &lt;code&gt;expo-file-system&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;package.json&lt;/code&gt; contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^54.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo-file-system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^55.0.16"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, the Expo SDK 54 documentation showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;expo-file-system ~19.0.23
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looked wrong. How could the package be version &lt;code&gt;55.0.16&lt;/code&gt; in my project while the documentation recommended &lt;code&gt;19.0.23&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The confusion became even greater when I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Installing ExpoFileSystem 19.0.23 (was 55.0.16)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build succeeded.&lt;/p&gt;

&lt;p&gt;At that moment I realized I didn't actually understand how Expo packages, native modules, CocoaPods, and version compatibility worked together.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Initial Assumption
&lt;/h2&gt;

&lt;p&gt;Like many JavaScript developers, I assumed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;expo-file-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Install the package and everything needed for it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This mental model works reasonably well for pure JavaScript libraries such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;axios
npm &lt;span class="nb"&gt;install &lt;/span&gt;zod
npm &lt;span class="nb"&gt;install &lt;/span&gt;date-fns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no native code involved. The package version is the package version.&lt;/p&gt;

&lt;p&gt;Expo modules are different.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the Layers
&lt;/h2&gt;

&lt;p&gt;An Expo package actually exists in multiple worlds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expo SDK
│
├── JavaScript Package
│   └── expo-file-system
│
├── Native iOS Module
│   └── ExpoFileSystem Pod
│
├── Native Android Module
│   └── Gradle Module
│
└── Expo Compatibility Matrix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;expo-file-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm only knows about the JavaScript package published to npm.&lt;/p&gt;

&lt;p&gt;It does not know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Which Expo SDK you're using&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which React Native version you're using&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which native iOS pod version is compatible&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which Android native implementation should be used&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its job is simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Install the latest package matching the version range.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing more.&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;code&gt;npx expo install&lt;/code&gt; Actually Does
&lt;/h2&gt;

&lt;p&gt;Expo provides its own installation command:&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 &lt;span class="nb"&gt;install &lt;/span&gt;expo-file-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command behaves differently.&lt;/p&gt;

&lt;p&gt;Before installing anything, Expo checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current Expo SDK Version
          ↓
Compatible Package Version
          ↓
Install That Version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expo SDK 54
        ↓
expo-file-system ~19.0.23
        ↓
Install 19.0.23
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of grabbing the newest package available on npm, Expo chooses the version tested and validated against the current SDK.&lt;/p&gt;

&lt;p&gt;This is why Expo documentation almost always recommends:&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 &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;expo-package&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;expo-package&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Then Why Did &lt;code&gt;pod install&lt;/code&gt; Fix It?
&lt;/h2&gt;

&lt;p&gt;This was the part that confused me the most.&lt;/p&gt;

&lt;p&gt;I expected CocoaPods to simply install whatever was in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's not what happens.&lt;/p&gt;

&lt;p&gt;During:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expo autolinking runs.&lt;/p&gt;

&lt;p&gt;Autolinking scans installed Expo modules and reads their native configuration files.&lt;/p&gt;

&lt;p&gt;A simplified view looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
      ↓
Expo Package
      ↓
Podspec
      ↓
CocoaPods
      ↓
Native Pod Installation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When CocoaPods encountered the Expo File System module, it discovered that the native pod version expected for SDK 54 was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExpoFileSystem 19.0.23
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and corrected the native dependency accordingly.&lt;/p&gt;

&lt;p&gt;That's why I saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Installing ExpoFileSystem 19.0.23 (was 55.0.16)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native side was effectively protecting itself from an incompatible version.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Important Lesson
&lt;/h2&gt;

&lt;p&gt;The successful build was actually misleading.&lt;/p&gt;

&lt;p&gt;The build succeeding does not mean the dependency setup is healthy.&lt;/p&gt;

&lt;p&gt;I still had a mismatch between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JavaScript Layer
expo-file-system 55.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Native Layer
ExpoFileSystem 19.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A future build, CI pipeline, runtime feature, or TypeScript API could easily break because the JavaScript package and native implementation are expecting different things.&lt;/p&gt;

&lt;p&gt;The fact that CocoaPods repaired the native side doesn't mean the project is correctly configured.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rule I Follow Now
&lt;/h2&gt;

&lt;p&gt;Whenever I add an Expo package:&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 &lt;span class="nb"&gt;install &lt;/span&gt;expo-file-system
npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-camera
npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-location
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever I add a normal JavaScript library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;axios
npm &lt;span class="nb"&gt;install &lt;/span&gt;zod
npm &lt;span class="nb"&gt;install &lt;/span&gt;lodash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple way to remember it is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the package belongs to the Expo ecosystem, let Expo decide the version. If it's a pure JavaScript dependency, let npm decide.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A Helpful Command I Learned
&lt;/h2&gt;

&lt;p&gt;If you inherit an existing Expo project and suspect version drift, run:&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 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expo will inspect installed Expo packages and align them with the currently installed SDK.&lt;/p&gt;

&lt;p&gt;Follow it with:&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 doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to identify remaining compatibility issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Mental Model
&lt;/h2&gt;

&lt;p&gt;The mental model that finally made everything click for me was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
    =
Install package

expo install
    =
Find compatible version
+
Install package

pod install
    =
Install native iOS dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool solves a different problem.&lt;/p&gt;

&lt;p&gt;The mistake is assuming npm understands Expo SDK compatibility. It doesn't.&lt;/p&gt;

&lt;p&gt;Expo does.&lt;/p&gt;

&lt;p&gt;And that small distinction is the reason a seemingly mysterious &lt;code&gt;pod install&lt;/code&gt; ended up teaching me how Expo's dependency system actually works.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
