<?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: George Fean</title>
    <description>The latest articles on DEV Community by George Fean (@gfean).</description>
    <link>https://dev.to/gfean</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%2F4063188%2F02bb9f49-3c6b-412e-a71b-643fd8a39a9a.png</url>
      <title>DEV Community: George Fean</title>
      <link>https://dev.to/gfean</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gfean"/>
    <language>en</language>
    <item>
      <title>OTA Pricing Is Part of Your Release Architecture</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Mon, 14 Sep 2026 21:32:20 +0000</pubDate>
      <link>https://dev.to/gfean/ota-pricing-is-part-of-your-release-architecture-5djj</link>
      <guid>https://dev.to/gfean/ota-pricing-is-part-of-your-release-architecture-5djj</guid>
      <description>&lt;p&gt;Pricing is usually treated as something engineering looks at after the architecture decision.&lt;/p&gt;

&lt;p&gt;Pick a service.&lt;/p&gt;

&lt;p&gt;Make sure it solves the technical problem.&lt;/p&gt;

&lt;p&gt;Then compare plans.&lt;/p&gt;

&lt;p&gt;For infrastructure that participates directly in production releases, I think that order misses something important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing a platform charges for becomes one of the things your team starts paying attention to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an OTA system charges by monthly active users, engineers notice audience growth.&lt;/p&gt;

&lt;p&gt;If it charges by bandwidth, they notice transferred bytes.&lt;/p&gt;

&lt;p&gt;If it charges by storage, they notice retained artifacts.&lt;/p&gt;

&lt;p&gt;If it charges by seats, they notice how many people have operational access.&lt;/p&gt;

&lt;p&gt;If it charges by requests or compute, they notice check frequency and processing volume.&lt;/p&gt;

&lt;p&gt;None of those models is inherently wrong.&lt;/p&gt;

&lt;p&gt;But each one creates a different operational interface between the product and the engineering team.&lt;/p&gt;

&lt;p&gt;For React Native OTA updates, that matters because release safety depends on behavior the pricing model can indirectly encourage or discourage.&lt;/p&gt;

&lt;h2&gt;
  
  
  OTA workload is not one-dimensional
&lt;/h2&gt;

&lt;p&gt;An OTA service does much more than store a JavaScript file.&lt;/p&gt;

&lt;p&gt;A production system may have to retain release artifacts, evaluate compatibility, process update checks, deliver bundles and assets, prepare patch artifacts, maintain release history, track rollout state, preserve rollback targets, and provide enough metadata to understand which JavaScript release was running during an incident.&lt;/p&gt;

&lt;p&gt;Those workloads scale differently.&lt;/p&gt;

&lt;p&gt;Storage grows with things such as artifact size, release frequency, runtime lines, projects, and retention policy.&lt;/p&gt;

&lt;p&gt;Bandwidth grows with the size of what is delivered and how many devices actually download it.&lt;/p&gt;

&lt;p&gt;Patch preparation follows publishing activity and artifact characteristics.&lt;/p&gt;

&lt;p&gt;Update-resolution traffic follows fleet behavior and client policy.&lt;/p&gt;

&lt;p&gt;Operational support may depend more on integration complexity and incident frequency than raw user count.&lt;/p&gt;

&lt;p&gt;Trying to compress all of that into one billing unit is necessarily an approximation.&lt;/p&gt;

&lt;p&gt;The interesting question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which pricing unit is objectively correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which approximation produces incentives we are comfortable building our release process around?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  MAU pricing measures product reach well
&lt;/h2&gt;

&lt;p&gt;Monthly Active Users is a familiar metric for a reason.&lt;/p&gt;

&lt;p&gt;It is easy to understand.&lt;/p&gt;

&lt;p&gt;Product teams already track it.&lt;/p&gt;

&lt;p&gt;For many SaaS products, it is a reasonable proxy for both value and workload.&lt;/p&gt;

&lt;p&gt;If more people use an analytics product, for example, more events are usually generated, retained, and queried.&lt;/p&gt;

&lt;p&gt;For OTA systems the relationship is looser.&lt;/p&gt;

&lt;p&gt;Two applications can have the same MAU count and produce very different release workloads.&lt;/p&gt;

&lt;p&gt;One might ship a compact JavaScript fix once a month.&lt;/p&gt;

&lt;p&gt;Another might maintain several native runtime lines, release several times a week, retain a long rollback history, and ship asset-heavy updates.&lt;/p&gt;

&lt;p&gt;Their audience measure is identical.&lt;/p&gt;

&lt;p&gt;Their OTA operational profiles are not.&lt;/p&gt;

&lt;p&gt;The reverse can happen too.&lt;/p&gt;

&lt;p&gt;An application can gain a large audience during a month in which the engineering team publishes no OTA release at all.&lt;/p&gt;

&lt;p&gt;The value of reliable release infrastructure becomes greater because the blast radius of a future mistake is larger.&lt;/p&gt;

&lt;p&gt;But artifact preparation did not suddenly multiply just because more users opened the application.&lt;/p&gt;

&lt;p&gt;That does not make MAU pricing unreasonable.&lt;/p&gt;

&lt;p&gt;It means MAU primarily measures audience scale, not the complete shape of OTA workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bandwidth pricing creates a different incentive
&lt;/h2&gt;

&lt;p&gt;Charging for transferred bytes is much closer to one real infrastructure cost.&lt;/p&gt;

&lt;p&gt;Large updates cost more to distribute than small ones.&lt;/p&gt;

&lt;p&gt;A million devices downloading a 5 MB payload obviously creates a different delivery profile from the same fleet receiving a tiny patch.&lt;/p&gt;

&lt;p&gt;That makes bandwidth a technically understandable meter.&lt;/p&gt;

&lt;p&gt;It can also influence release behavior.&lt;/p&gt;

&lt;p&gt;If every full-bundle fallback has an obvious financial cost, teams may feel pressure to optimize transfer size aggressively.&lt;/p&gt;

&lt;p&gt;Usually that is healthy.&lt;/p&gt;

&lt;p&gt;Sometimes it can conflict with reliability.&lt;/p&gt;

&lt;p&gt;A patch is useful when the device has the correct baseline and reconstruction succeeds.&lt;/p&gt;

&lt;p&gt;A complete bundle is useful when the device does not.&lt;/p&gt;

&lt;p&gt;If the safest recovery path is financially treated as an expensive exception, the pricing model has entered the release-design discussion.&lt;/p&gt;

&lt;p&gt;Again, that does not make bandwidth pricing bad.&lt;/p&gt;

&lt;p&gt;It means the team should ask whether the meter aligns with the failure behavior it actually wants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage pricing encourages explicit retention decisions
&lt;/h2&gt;

&lt;p&gt;Storage is another understandable unit.&lt;/p&gt;

&lt;p&gt;OTA artifacts occupy real space.&lt;/p&gt;

&lt;p&gt;Previous releases are useful for rollback.&lt;/p&gt;

&lt;p&gt;Source maps and release metadata are useful during incident analysis.&lt;/p&gt;

&lt;p&gt;Multiple runtime lines can remain active while users slowly adopt a new store binary.&lt;/p&gt;

&lt;p&gt;A storage-oriented model makes that history visible.&lt;/p&gt;

&lt;p&gt;When storage approaches a limit, the engineering team can inspect what it is retaining.&lt;/p&gt;

&lt;p&gt;Perhaps old development releases can be removed.&lt;/p&gt;

&lt;p&gt;Perhaps production history should be kept longer.&lt;/p&gt;

&lt;p&gt;Perhaps an obsolete runtime line can finally be retired.&lt;/p&gt;

&lt;p&gt;Those are actions engineers can connect directly to release management.&lt;/p&gt;

&lt;p&gt;Storage is not a perfect meter either.&lt;/p&gt;

&lt;p&gt;A small set of heavily downloaded bundles can occupy very little disk while generating substantial traffic.&lt;/p&gt;

&lt;p&gt;A platform still has databases, request processing, monitoring, support, and other operational costs.&lt;/p&gt;

&lt;p&gt;The benefit is not perfect cost correspondence.&lt;/p&gt;

&lt;p&gt;It is that the unit maps to something the release team can inspect and control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seat pricing changes who participates in releases
&lt;/h2&gt;

&lt;p&gt;Seat-based pricing is common because it is predictable.&lt;/p&gt;

&lt;p&gt;The company knows roughly how many people need access.&lt;/p&gt;

&lt;p&gt;The provider can tie plans to organization size.&lt;/p&gt;

&lt;p&gt;But it also creates an incentive.&lt;/p&gt;

&lt;p&gt;If every additional QA engineer, support engineer, or incident responder increases the bill, organizations may restrict access to the release system.&lt;/p&gt;

&lt;p&gt;That may be completely acceptable.&lt;/p&gt;

&lt;p&gt;It may also work against a release process that benefits from wider operational visibility.&lt;/p&gt;

&lt;p&gt;An OTA dashboard is not only used by the person who presses Publish.&lt;/p&gt;

&lt;p&gt;During an incident, support may need to identify which release is active.&lt;/p&gt;

&lt;p&gt;QA may need to verify a rollback.&lt;/p&gt;

&lt;p&gt;Another engineer may need to inspect targeting or runtime compatibility.&lt;/p&gt;

&lt;p&gt;Pricing access is therefore also pricing participation in release operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combined usage models can be technically accurate and cognitively expensive
&lt;/h2&gt;

&lt;p&gt;A provider could meter everything:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;storage
+ bandwidth
+ requests
+ build or patch compute
+ seats
+ support tier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That may produce a more accurate representation of infrastructure cost.&lt;/p&gt;

&lt;p&gt;It can also produce a worse engineering experience.&lt;/p&gt;

&lt;p&gt;Before publishing a production fix, nobody wants to calculate a miniature cloud-capacity model to understand whether the release changes the monthly bill materially.&lt;/p&gt;

&lt;p&gt;There is always a tradeoff between technical precision and predictability.&lt;/p&gt;

&lt;p&gt;A pricing model can model provider cost extremely well while giving customers a difficult system to reason about.&lt;/p&gt;

&lt;p&gt;Or it can intentionally absorb variation behind a simpler capacity boundary.&lt;/p&gt;

&lt;p&gt;Both approaches are business choices.&lt;/p&gt;

&lt;p&gt;What matters to engineering is the behavior they create.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing can influence release granularity
&lt;/h2&gt;

&lt;p&gt;This is where the discussion becomes more than finance.&lt;/p&gt;

&lt;p&gt;Suppose a team finds a small JavaScript bug in production.&lt;/p&gt;

&lt;p&gt;The technically clean response might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fix the narrow issue;&lt;/li&gt;
&lt;li&gt;validate the exact artifact;&lt;/li&gt;
&lt;li&gt;release it to a small cohort;&lt;/li&gt;
&lt;li&gt;observe the result;&lt;/li&gt;
&lt;li&gt;expand it when healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine the team has started thinking of every release primarily through a usage threshold.&lt;/p&gt;

&lt;p&gt;A conversation can subtly change from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this fix compatible and safe?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this change important enough to justify another OTA event this month?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That can encourage teams to combine unrelated changes.&lt;/p&gt;

&lt;p&gt;Or wait for a larger release.&lt;/p&gt;

&lt;p&gt;Or avoid a conservative staged rollout because additional delivery feels expensive.&lt;/p&gt;

&lt;p&gt;No pricing model mechanically forces those decisions.&lt;/p&gt;

&lt;p&gt;Engineering culture matters much more.&lt;/p&gt;

&lt;p&gt;But incentives do matter.&lt;/p&gt;

&lt;p&gt;Release infrastructure works best when the easiest economic behavior and the safest technical behavior point in roughly the same direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing can influence rollback history too
&lt;/h2&gt;

&lt;p&gt;Rollback safety depends on keeping something worth returning to.&lt;/p&gt;

&lt;p&gt;If previous artifacts are immediately discarded, the release system has fewer known-good options.&lt;/p&gt;

&lt;p&gt;If everything is retained forever, storage and operational history grow indefinitely.&lt;/p&gt;

&lt;p&gt;There is a real tradeoff.&lt;/p&gt;

&lt;p&gt;A useful pricing model makes the tradeoff visible enough that teams manage it deliberately rather than accidentally.&lt;/p&gt;

&lt;p&gt;For OTA, that means questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many production releases should remain recoverable?&lt;/li&gt;
&lt;li&gt;How long do older runtime lines stay active?&lt;/li&gt;
&lt;li&gt;Which source maps need to remain available for crash investigation?&lt;/li&gt;
&lt;li&gt;Which development artifacts are genuinely disposable?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are release-engineering decisions.&lt;/p&gt;

&lt;p&gt;They are easier to reason about when the resource being limited has a visible relationship to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  User growth is different from release growth
&lt;/h2&gt;

&lt;p&gt;One of the reasons I think OTA pricing deserves architectural attention is that application success and release-system complexity are not the same thing.&lt;/p&gt;

&lt;p&gt;A team can double its user base without doubling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the number of projects;&lt;/li&gt;
&lt;li&gt;the number of runtime lines;&lt;/li&gt;
&lt;li&gt;the number of retained artifacts;&lt;/li&gt;
&lt;li&gt;the number of engineers publishing releases;&lt;/li&gt;
&lt;li&gt;or the number of releases shipped that month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traffic can certainly increase with audience size.&lt;/p&gt;

&lt;p&gt;More devices may check for updates.&lt;/p&gt;

&lt;p&gt;A popular release may transfer much more data.&lt;/p&gt;

&lt;p&gt;Support expectations can rise.&lt;/p&gt;

&lt;p&gt;The relationship is real.&lt;/p&gt;

&lt;p&gt;It simply is not one-to-one.&lt;/p&gt;

&lt;p&gt;That distinction matters when forecasting operational cost.&lt;/p&gt;

&lt;p&gt;If the meter is audience-based, growth forecasting becomes part of OTA budgeting.&lt;/p&gt;

&lt;p&gt;If the meter is storage-based, artifact and retention planning becomes more important.&lt;/p&gt;

&lt;p&gt;If the meter is bandwidth-based, bundle size and adoption dominate.&lt;/p&gt;

&lt;p&gt;The pricing model tells the engineering organization which variable it will spend time predicting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful evaluation question for any OTA provider
&lt;/h2&gt;

&lt;p&gt;When comparing OTA systems, I would add one question beside compatibility, rollback, and delivery:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What behavior does this pricing model cause our team to optimize?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For an MAU model, ask how active users are defined.&lt;/p&gt;

&lt;p&gt;Are they accounts?&lt;/p&gt;

&lt;p&gt;Devices?&lt;/p&gt;

&lt;p&gt;Anonymous installations?&lt;/p&gt;

&lt;p&gt;Only users who actually request an update?&lt;/p&gt;

&lt;p&gt;For bandwidth, ask whether full-bundle fallback, retries, and assets are included.&lt;/p&gt;

&lt;p&gt;For storage, ask what artifacts count and how retention works.&lt;/p&gt;

&lt;p&gt;For seats, ask which operational roles need access.&lt;/p&gt;

&lt;p&gt;For combined usage, model one realistic release month rather than comparing headline plan prices.&lt;/p&gt;

&lt;p&gt;The best meter is not necessarily the smallest number.&lt;/p&gt;

&lt;p&gt;It is the one whose surprises your team knows how to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing should not override release safety
&lt;/h2&gt;

&lt;p&gt;Regardless of billing model, some release rules should remain non-negotiable.&lt;/p&gt;

&lt;p&gt;If a change crosses the native compatibility boundary, it needs a new binary.&lt;/p&gt;

&lt;p&gt;If a candidate has not been validated, it should not become safer because delaying it saves money.&lt;/p&gt;

&lt;p&gt;If risk calls for staged rollout, use staged rollout.&lt;/p&gt;

&lt;p&gt;If a bad release needs a full known-good fallback, reliability should win over optimizing transferred bytes.&lt;/p&gt;

&lt;p&gt;If production history is necessary for rollback and incident review, retain enough of it.&lt;/p&gt;

&lt;p&gt;The pricing model belongs around those decisions.&lt;/p&gt;

&lt;p&gt;It should not silently rewrite them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Bundle Drop fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: I build Bundle Drop.&lt;/p&gt;

&lt;p&gt;Bundle Drop currently does not meter plans by monthly active users.&lt;/p&gt;

&lt;p&gt;The public plans all include unlimited MAU, while visible capacity is expressed through things such as projects, organization members, channels, and shared OTA storage. &lt;a href="https://bundledrop.app/pricing" rel="noopener noreferrer"&gt;The pricing page&lt;/a&gt; remains the source of truth because those plan details can change.&lt;/p&gt;

&lt;p&gt;That is not a claim that MAU pricing is universally wrong.&lt;/p&gt;

&lt;p&gt;It is a product decision about which behavior I want Bundle Drop users to think about.&lt;/p&gt;

&lt;p&gt;I would rather a team ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this release runtime-compatible?&lt;/li&gt;
&lt;li&gt;Was this exact artifact validated?&lt;/li&gt;
&lt;li&gt;Should we roll it out gradually?&lt;/li&gt;
&lt;li&gt;Do we have enough known-good history to recover?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;than ask whether another successful month of user growth changes whether a small production fix is worth shipping.&lt;/p&gt;

&lt;p&gt;Resource-oriented capacity is an approximation too.&lt;/p&gt;

&lt;p&gt;Two applications with the same retained storage can generate very different traffic and support workloads.&lt;/p&gt;

&lt;p&gt;Bundle Drop accepts more of that variation behind the same visible capacity because the resulting engineering incentive is the one I prefer for the product.&lt;/p&gt;

&lt;p&gt;Other providers can reasonably make a different tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing is part of the engineering interface
&lt;/h2&gt;

&lt;p&gt;OTA pricing is not just the invoice attached to the release platform.&lt;/p&gt;

&lt;p&gt;It tells engineering which variable is scarce.&lt;/p&gt;

&lt;p&gt;Audience.&lt;/p&gt;

&lt;p&gt;Bytes.&lt;/p&gt;

&lt;p&gt;Storage.&lt;/p&gt;

&lt;p&gt;Access.&lt;/p&gt;

&lt;p&gt;Requests.&lt;/p&gt;

&lt;p&gt;Compute.&lt;/p&gt;

&lt;p&gt;Every choice creates a different optimization target.&lt;/p&gt;

&lt;p&gt;When evaluating an OTA system, the useful question is therefore not only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much will this cost us?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What will this pricing model make our team think about every time we ship?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That answer becomes part of the release architecture whether we intend it to or not.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://bundledrop.app/resources/why-we-dont-price-by-monthly-active-users" rel="noopener noreferrer"&gt;Bundle Drop Resources&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>devops</category>
      <category>architecture</category>
      <category>mobile</category>
    </item>
    <item>
      <title>React Native OTA After CodePush: How to Choose a Tool in 2026</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:15:05 +0000</pubDate>
      <link>https://dev.to/gfean/react-native-ota-after-codepush-how-to-choose-a-tool-in-2026-46of</link>
      <guid>https://dev.to/gfean/react-native-ota-after-codepush-how-to-choose-a-tool-in-2026-46of</guid>
      <description>&lt;p&gt;Microsoft’s hosted App Center CodePush service is gone.&lt;/p&gt;

&lt;p&gt;App Center’s main services, including CodePush, were retired on March 31, 2025. Microsoft also published a standalone CodePush server for teams that wanted to preserve the old protocol, but that repository was archived and made read-only in May 2025. (&lt;a href="https://learn.microsoft.com/en-us/appcenter/retirement" rel="noopener noreferrer"&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So React Native teams evaluating OTA in 2026 are no longer choosing between “CodePush or nothing.”&lt;/p&gt;

&lt;p&gt;They are choosing between several different operating models.&lt;/p&gt;

&lt;p&gt;Some tools preserve a CodePush-shaped workflow.&lt;/p&gt;

&lt;p&gt;Some are built around Expo’s update protocol.&lt;/p&gt;

&lt;p&gt;Some use their own SDK, runtime, and rollout model.&lt;/p&gt;

&lt;p&gt;Others move the whole service into infrastructure your team operates.&lt;/p&gt;

&lt;p&gt;That makes “Which CodePush alternative is best?” a fairly weak question.&lt;/p&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which system matches the compatibility model, release controls, migration distance, recovery behavior, and ownership boundary our team actually needs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not a ranking. Product behavior and plans change, and similarly named features often have different semantics. The first-party documentation referenced here was rechecked in September 2026. (&lt;a href="https://bundledrop.app/resources/react-native-ota-update-tools-2026" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the operating model
&lt;/h2&gt;

&lt;p&gt;The first split is not really about features.&lt;/p&gt;

&lt;p&gt;It is about who owns the OTA platform.&lt;/p&gt;

&lt;p&gt;A managed service gives the mobile team a hosted control plane, artifact infrastructure, and supported release workflow.&lt;/p&gt;

&lt;p&gt;A self-hosted system gives the organization more infrastructure control, but also makes it responsible for availability, upgrades, security patching, backups, compatibility with old mobile clients, and incident response.&lt;/p&gt;

&lt;p&gt;Neither model is automatically better.&lt;/p&gt;

&lt;p&gt;The important part is deciding whether infrastructure ownership is a real requirement or simply something that sounds attractive during evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The current shortlist looks roughly like this
&lt;/h2&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;Model&lt;/th&gt;
&lt;th&gt;Why a team might shortlist it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Expo EAS Update&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed Expo service&lt;/td&gt;
&lt;td&gt;A natural starting point when EAS already owns builds and releases. It uses &lt;code&gt;expo-updates&lt;/code&gt;, runtime versions, channels and branches, supports gradual rollouts, and can roll back to a previous or embedded update. Hosted usage currently meters MAU and global edge bandwidth. (&lt;a href="https://docs.expo.dev/eas-update/introduction/" rel="noopener noreferrer"&gt;Expo Documentation&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codemagic CodePush&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed CodePush service&lt;/td&gt;
&lt;td&gt;Keeps familiar CodePush concepts such as iOS/Android apps, Staging/Production deployments, target binary versions, promotion, percentage rollout, and rollback. Codemagic currently offers both successful-install and fixed-MAU pricing models. (&lt;a href="https://docs.codemagic.io/rn-codepush/releasing-updates/" rel="noopener noreferrer"&gt;Codemagic Documentation&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AppsOnAir CodePush&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed CodePush-style service&lt;/td&gt;
&lt;td&gt;Preserves a CodePush-shaped CLI and release model with target binary versions, percentage rollout, rollback, release metrics, immutable releases, and code signing. (&lt;a href="https://documentation.appsonair.com/MobileQuickstart/CodePush/codepush-faqs" rel="noopener noreferrer"&gt;AppsOnAir&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React Native Stallion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed service, with self-hosted enterprise option&lt;/td&gt;
&lt;td&gt;Uses its own SDK, CLI, console, promotion workflow, phased rollout, pause/rollback and adoption tooling. Hosted plans currently use MAU and bandwidth allowances, while enterprise options include self-hosted deployment. (&lt;a href="https://stalliontech.io/learn/docs/introduction" rel="noopener noreferrer"&gt;React Native Stallion&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Revopush&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed CodePush-shaped service&lt;/td&gt;
&lt;td&gt;A candidate for teams that want a CodePush-style migration but need modern React Native/New Architecture and Expo paths. Current docs use Revopush’s own newer SDK line for modern React Native, with rollout, client/server rollback, signing, and CI/CD integrations. Hosted pricing currently combines MAU and egress allowances. (&lt;a href="https://docs.revopush.org/" rel="noopener noreferrer"&gt;Revopush&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hot Updater&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-hostable, plugin-based&lt;/td&gt;
&lt;td&gt;Designed for teams that want to choose their own storage, database, build and hosting providers. Its current model includes channels, rollback, compatibility/fingerprint controls, and a pluggable infrastructure architecture. (&lt;a href="https://hot-updater.dev/docs/get-started/introduction" rel="noopener noreferrer"&gt;Hot Updater&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standalone CodePush server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Archived self-hosted CodePush&lt;/td&gt;
&lt;td&gt;Keeps the classic CodePush protocol available for teams prepared to maintain it themselves. The Microsoft repository is archived, read-only, and unsupported, so the team owns ongoing infrastructure and maintenance. (&lt;a href="https://github.com/microsoft/code-push-server" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bundle Drop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed OTA service&lt;/td&gt;
&lt;td&gt;Uses explicit runtime compatibility, channels, staged/property-targeted rollout, local recovery, and patch/full-bundle delivery. Current pricing does not meter MAU; plans are capacity-oriented instead. (&lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is useful for building a shortlist.&lt;/p&gt;

&lt;p&gt;It is not enough to make the decision.&lt;/p&gt;

&lt;p&gt;Two platforms can both say “percentage rollout” while using different device identities, fallback rules, and constraints.&lt;/p&gt;

&lt;p&gt;Two platforms can both say “rollback” while one means server-side release history and another also has device-local recovery.&lt;/p&gt;

&lt;p&gt;Two platforms can both say “patch updates” while using very different baselines and fallback behavior.&lt;/p&gt;

&lt;p&gt;The semantics matter more than the checkmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Test native compatibility before anything else
&lt;/h2&gt;

&lt;p&gt;OTA only works safely inside a native compatibility boundary.&lt;/p&gt;

&lt;p&gt;The installed binary must already contain every native capability the JavaScript update needs.&lt;/p&gt;

&lt;p&gt;Different systems express this differently.&lt;/p&gt;

&lt;p&gt;Expo uses runtime versions, and its update policy requires platform and runtime to match. (&lt;a href="https://docs.expo.dev/eas-update/how-it-works/" rel="noopener noreferrer"&gt;Expo Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;CodePush-style systems historically use target binary version ranges.&lt;/p&gt;

&lt;p&gt;Bundle Drop uses explicit per-platform runtime versions.&lt;/p&gt;

&lt;p&gt;Hot Updater documents a fingerprint-based compatibility strategy.&lt;/p&gt;

&lt;p&gt;The terminology matters less than the invariant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;an update must not reach a binary that cannot execute it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During evaluation, deliberately change a native dependency or permission and prove that the old binary cannot accidentally receive JavaScript that depends on the new native state.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Measure migration distance
&lt;/h2&gt;

&lt;p&gt;A team already running Microsoft CodePush has different priorities from a team starting fresh.&lt;/p&gt;

&lt;p&gt;If preserving deployment keys, Staging/Production deployments, CLI commands, and the CodePush mental model matters, CodePush-compatible services deserve early attention.&lt;/p&gt;

&lt;p&gt;Codemagic and AppsOnAir deliberately preserve much of that model. Revopush also offers a CodePush-shaped migration, although its current documentation uses Revopush’s newer SDK for newer React Native/New Architecture combinations. (&lt;a href="https://docs.codemagic.io/rn-codepush/cli-quick-reference/" rel="noopener noreferrer"&gt;Codemagic Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;EAS Update is a different migration.&lt;/p&gt;

&lt;p&gt;You adopt &lt;code&gt;expo-updates&lt;/code&gt;, Expo runtime semantics, and the EAS channel/branch model. (&lt;a href="https://docs.expo.dev/eas-update/getting-started/" rel="noopener noreferrer"&gt;Expo Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Bundle Drop, Stallion, and Hot Updater likewise introduce their own client and release models.&lt;/p&gt;

&lt;p&gt;None of those is inherently worse.&lt;/p&gt;

&lt;p&gt;But migration distance is a real engineering cost and should be treated as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test release-selection semantics
&lt;/h2&gt;

&lt;p&gt;Do not stop after proving that a 10% rollout “works.”&lt;/p&gt;

&lt;p&gt;Test what happens to a device that is &lt;strong&gt;not&lt;/strong&gt; in the 10%.&lt;/p&gt;

&lt;p&gt;Does it stay on its current release?&lt;/p&gt;

&lt;p&gt;Does it receive the previous eligible release?&lt;/p&gt;

&lt;p&gt;Can another partially rolled-out release also be active?&lt;/p&gt;

&lt;p&gt;Can the rollout percentage decrease?&lt;/p&gt;

&lt;p&gt;How is cohort membership kept stable?&lt;/p&gt;

&lt;p&gt;Stallion, for example, explicitly documents that the resolver checks the latest candidate and then at most the previous eligible release; if that previous release is itself partial, the user can receive no release. (&lt;a href="https://stalliontech.io/learn/docs/phased-rollout" rel="noopener noreferrer"&gt;React Native Stallion&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Codemagic’s CodePush model permits one active partial rollout per deployment and requires that rollout to be completed or stopped before another release proceeds. (&lt;a href="https://docs.codemagic.io/rn-codepush/production-control/" rel="noopener noreferrer"&gt;Codemagic Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Expo’s current rollout model includes both per-update and branch-based rollouts. (&lt;a href="https://docs.expo.dev/eas-update/deployment/" rel="noopener noreferrer"&gt;Expo Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;These are all legitimate designs.&lt;/p&gt;

&lt;p&gt;They are not interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Separate server rollback from device recovery
&lt;/h2&gt;

&lt;p&gt;A dashboard rollback protects future release selection.&lt;/p&gt;

&lt;p&gt;It does not automatically rescue a device that already activated a broken update and cannot get far enough to perform another successful update check.&lt;/p&gt;

&lt;p&gt;This distinction is worth testing explicitly.&lt;/p&gt;

&lt;p&gt;Revopush’s documentation, for example, describes both client-side automatic rollback and server-side rollback. (&lt;a href="https://docs.revopush.org/sdk/getting-started" rel="noopener noreferrer"&gt;Revopush&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Codemagic’s CodePush documentation similarly separates automatic client rollback from manual server rollback. (&lt;a href="https://docs.codemagic.io/rn-codepush/production-control/" rel="noopener noreferrer"&gt;Codemagic Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Bundle Drop’s current SDK records startup attempts natively, can quarantine a repeatedly failing candidate locally, and falls back to a known-good OTA or embedded bundle without requiring the network during startup. (&lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Whatever product you evaluate, test both failure windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Treat patches as transport, not release identity
&lt;/h2&gt;

&lt;p&gt;Differential delivery is useful because many OTA updates change only a small part of the final artifact.&lt;/p&gt;

&lt;p&gt;But production fleets do not all have the same history.&lt;/p&gt;

&lt;p&gt;Some users skip releases.&lt;/p&gt;

&lt;p&gt;Some reinstall.&lt;/p&gt;

&lt;p&gt;Some roll back.&lt;/p&gt;

&lt;p&gt;Some remain outside a staged cohort.&lt;/p&gt;

&lt;p&gt;So ask what happens when a patch baseline is unavailable.&lt;/p&gt;

&lt;p&gt;Can the client obtain a complete artifact instead?&lt;/p&gt;

&lt;p&gt;How is the reconstructed result verified?&lt;/p&gt;

&lt;p&gt;What happens after an interrupted patch?&lt;/p&gt;

&lt;p&gt;The safe release identity should not depend on one optimization path succeeding.&lt;/p&gt;

&lt;p&gt;This is one area where product implementations differ substantially, so it deserves a device test rather than a feature-matrix check.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Check observability before the first incident
&lt;/h2&gt;

&lt;p&gt;Once OTA exists, app version alone no longer identifies the running JavaScript.&lt;/p&gt;

&lt;p&gt;Two users may both report native version &lt;code&gt;4.3.0&lt;/code&gt; while running different OTA releases.&lt;/p&gt;

&lt;p&gt;A useful system should let you connect:&lt;/p&gt;

&lt;p&gt;release identity,&lt;/p&gt;

&lt;p&gt;runtime compatibility,&lt;/p&gt;

&lt;p&gt;download/install state,&lt;/p&gt;

&lt;p&gt;crash reports,&lt;/p&gt;

&lt;p&gt;rollout state,&lt;/p&gt;

&lt;p&gt;and rollback outcomes.&lt;/p&gt;

&lt;p&gt;If an error tracker says “4.3.0 crashed,” you should be able to determine which JavaScript artifact was actually executing.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Compare total operating cost, not the cheapest headline price
&lt;/h2&gt;

&lt;p&gt;OTA pricing currently uses several models.&lt;/p&gt;

&lt;p&gt;Expo meters MAU and global edge bandwidth. (&lt;a href="https://docs.expo.dev/billing/usage-based-pricing/" rel="noopener noreferrer"&gt;Expo Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Codemagic offers successful-install and fixed-MAU models. (&lt;a href="https://docs.codemagic.io/billing/pricing/" rel="noopener noreferrer"&gt;Codemagic Documentation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Revopush currently combines MAU and egress allowances. (&lt;a href="https://revopush.org/pricing" rel="noopener noreferrer"&gt;Revopush&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Stallion’s hosted tiers combine MAU and bandwidth allowances. (&lt;a href="https://stalliontech.io/pricing" rel="noopener noreferrer"&gt;React Native Stallion&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Bundle Drop currently does not price plans by MAU and instead uses capacity-oriented limits such as storage, projects, and organization size. (&lt;a href="https://bundledrop.app/" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Self-hosted software moves part of the bill elsewhere: infrastructure, engineering time, monitoring, backups, security maintenance, and on-call.&lt;/p&gt;

&lt;p&gt;The useful cost model therefore uses your actual release pattern:&lt;/p&gt;

&lt;p&gt;bundle sizes,&lt;/p&gt;

&lt;p&gt;release frequency,&lt;/p&gt;

&lt;p&gt;number of active installations,&lt;/p&gt;

&lt;p&gt;expected adoption,&lt;/p&gt;

&lt;p&gt;storage retention,&lt;/p&gt;

&lt;p&gt;and operational ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better evaluation than a feature matrix
&lt;/h2&gt;

&lt;p&gt;If I were comparing OTA platforms for a production React Native app, I would put two or three candidates through the same acceptance test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Integrate each into an actual iOS and Android &lt;strong&gt;release&lt;/strong&gt; build, not only a development client.&lt;/li&gt;
&lt;li&gt;Publish a harmless JavaScript + asset update and prove exact native/runtime compatibility.&lt;/li&gt;
&lt;li&gt;Start a partial rollout and verify cohort stability and fallback behavior for an installation that is not selected.&lt;/li&gt;
&lt;li&gt;Interrupt or invalidate delivery and confirm the currently working bundle stays active.&lt;/li&gt;
&lt;li&gt;Activate a deliberately failing candidate, test local recovery, then perform a separate server rollback.&lt;/li&gt;
&lt;li&gt;Verify that crash reports and release telemetry identify the exact active JavaScript artifact.&lt;/li&gt;
&lt;li&gt;Model one year of cost using the application’s expected update cadence and operating requirements.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The winner should be the system whose behavior your team can explain and operate.&lt;/p&gt;

&lt;p&gt;Not the product with the longest feature table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Bundle Drop fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: I build Bundle Drop, so it is one of the products in this comparison.&lt;/p&gt;

&lt;p&gt;Bundle Drop is currently a managed option for Expo and bare React Native, with explicit native/runtime compatibility, named channels, staged and property-targeted release selection, native startup recovery, and hybrid patch/full-bundle delivery. Its public npm package is currently &lt;code&gt;0.7.0&lt;/code&gt;. (&lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;It is not the natural choice for every team.&lt;/p&gt;

&lt;p&gt;If EAS already owns your release workflow and you want to stay inside the Expo update protocol, EAS Update should be near the top of the shortlist.&lt;/p&gt;

&lt;p&gt;If preserving the existing CodePush mental model minimizes your migration cost, CodePush-compatible managed services deserve serious evaluation.&lt;/p&gt;

&lt;p&gt;If self-hosting is a hard infrastructure requirement, evaluate systems designed around that ownership model.&lt;/p&gt;

&lt;p&gt;The useful goal is not finding a universal CodePush successor.&lt;/p&gt;

&lt;p&gt;There isn’t one.&lt;/p&gt;

&lt;p&gt;The useful goal is choosing an OTA system whose compatibility, release-selection, recovery, delivery, observability, and ownership semantics match the way your team actually ships React Native applications.&lt;/p&gt;




&lt;p&gt;The first-party pages I used for the verification pass are available here: &lt;a href="https://learn.microsoft.com/en-us/appcenter/retirement" rel="noopener noreferrer"&gt;Microsoft App Center retirement notice&lt;/a&gt;, &lt;a href="https://docs.expo.dev/eas-update/introduction/" rel="noopener noreferrer"&gt;Expo EAS Update documentation&lt;/a&gt;, &lt;a href="https://docs.codemagic.io/rn-codepush/production-control/" rel="noopener noreferrer"&gt;Codemagic CodePush production controls&lt;/a&gt;, &lt;a href="https://documentation.appsonair.com/MobileQuickstart/CodePush/codepush-faqs" rel="noopener noreferrer"&gt;AppsOnAir CodePush FAQ&lt;/a&gt;, &lt;a href="https://stalliontech.io/learn/docs/introduction" rel="noopener noreferrer"&gt;React Native Stallion documentation&lt;/a&gt;, &lt;a href="https://docs.revopush.org/" rel="noopener noreferrer"&gt;Revopush documentation&lt;/a&gt;, &lt;a href="https://hot-updater.dev/docs/get-started/introduction" rel="noopener noreferrer"&gt;Hot Updater documentation&lt;/a&gt;, and &lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;Bundle Drop npm package&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Compare React Native Bundles Between Releases</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Thu, 03 Sep 2026 01:36:52 +0000</pubDate>
      <link>https://dev.to/gfean/how-to-compare-react-native-bundles-between-releases-42lb</link>
      <guid>https://dev.to/gfean/how-to-compare-react-native-bundles-between-releases-42lb</guid>
      <description>&lt;p&gt;A React Native bundle-size regression usually starts with a disappointingly vague observation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The bundle got bigger.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe yesterday’s Android production bundle was smaller.&lt;/p&gt;

&lt;p&gt;Maybe a pull request added a dependency.&lt;/p&gt;

&lt;p&gt;Maybe a framework upgrade changed Metro output.&lt;/p&gt;

&lt;p&gt;Maybe one feature imported far more code than expected.&lt;/p&gt;

&lt;p&gt;The total size tells you that &lt;em&gt;something&lt;/em&gt; changed.&lt;/p&gt;

&lt;p&gt;It does not tell you what changed.&lt;/p&gt;

&lt;p&gt;That distinction is why I think bundle analysis becomes much more useful when it is treated as a &lt;strong&gt;comparison problem&lt;/strong&gt;, not just a visualization problem.&lt;/p&gt;

&lt;p&gt;A treemap can answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is inside this build?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A proper comparison should answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed between these two builds?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are different questions.&lt;/p&gt;

&lt;p&gt;Bundle Drop Sight supports both workflows, but the approach itself is useful regardless of which analyzer you use: generate comparable production artifacts, attribute the output through matching source maps, and compare source identities rather than eyeballing two unrelated screenshots. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with two complete build pairs
&lt;/h2&gt;

&lt;p&gt;A React Native comparison is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old.bundle
vs
new.bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each bundle needs its own matching source map.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Baseline build
├── production JS bundle
└── matching source map

Current build
├── production JS bundle
└── matching source map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source map matters because the generated JavaScript bundle no longer maps cleanly to the original TypeScript or JavaScript files by inspection alone.&lt;/p&gt;

&lt;p&gt;Metro transforms modules, wraps code, resolves dependencies, and emits generated JavaScript.&lt;/p&gt;

&lt;p&gt;The source map connects generated positions back to source identities.&lt;/p&gt;

&lt;p&gt;That lets an analyzer ask a narrower and much more useful question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many bytes in this generated bundle are associated with each mapped source?&lt;/strong&gt; (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Those numbers describe generated JavaScript ownership.&lt;/p&gt;

&lt;p&gt;They are not the file sizes of your original &lt;code&gt;.tsx&lt;/code&gt; files, and they are not the size of the final &lt;code&gt;.ipa&lt;/code&gt;, &lt;code&gt;.apk&lt;/code&gt;, or &lt;code&gt;.aab&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That distinction is important when interpreting the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source map has to match the bundle
&lt;/h2&gt;

&lt;p&gt;Source maps are positional.&lt;/p&gt;

&lt;p&gt;A map from one build can still be perfectly valid JSON while being the wrong map for another build.&lt;/p&gt;

&lt;p&gt;A tiny code change can shift generated positions throughout a minified bundle.&lt;/p&gt;

&lt;p&gt;So the baseline bundle needs the baseline source map.&lt;/p&gt;

&lt;p&gt;The candidate bundle needs the candidate source map.&lt;/p&gt;

&lt;p&gt;For useful comparisons, also keep the build conditions equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same platform
same entry point
same Metro configuration
same development flag
same minification setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you compare an Android production bundle against an iOS bundle, or a minified build against a differently configured build, the delta may be dominated by the build process instead of the code change you are investigating. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;There is one obvious exception: if the Metro or build configuration change is exactly what you are trying to measure, then changing that variable is intentional.&lt;/p&gt;

&lt;p&gt;Otherwise, keep the environment boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyze both builds before comparing them
&lt;/h2&gt;

&lt;p&gt;A useful comparison begins by treating each build as a complete analysis.&lt;/p&gt;

&lt;p&gt;For each bundle, generated bytes can be grouped into areas such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application code
Dependencies
Metro / runtime
Unmapped output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because a total delta alone can still hide the interesting part.&lt;/p&gt;

&lt;p&gt;Suppose the bundle increased by 150 KB.&lt;/p&gt;

&lt;p&gt;That might actually mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application code     +90 KB
Dependencies        +180 KB
Metro/runtime          0 KB
Unmapped             -20 KB
Removed dependency  -100 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there is something to investigate.&lt;/p&gt;

&lt;p&gt;The total change is only the final sum.&lt;/p&gt;

&lt;p&gt;The category changes tell you which direction to look first. Sight’s current attribution model keeps application code, dependencies, runtime output, and unmapped bytes explicit instead of silently discarding unattributed output. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Match source identities, not treemap positions
&lt;/h2&gt;

&lt;p&gt;One mistake I would avoid is opening two treemaps side by side and trying to visually spot the difference.&lt;/p&gt;

&lt;p&gt;Treemap layouts are relative.&lt;/p&gt;

&lt;p&gt;If one package grows, several unrelated rectangles may move or change shape even though their own size barely changed.&lt;/p&gt;

&lt;p&gt;Rankings can also move.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;baseline screenshot
+
current screenshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is a weak diff.&lt;/p&gt;

&lt;p&gt;A better comparison matches stable identities.&lt;/p&gt;

&lt;p&gt;Sight Compare currently analyzes the baseline and current build independently, then matches normalized package and file identities.&lt;/p&gt;

&lt;p&gt;For each matched entry it can record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before bytes
after bytes
signed delta
percentage change
state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where state can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;added
removed
increased
decreased
unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the investigation much closer to a source diff than a visual guessing exercise. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Sort by absolute change first
&lt;/h2&gt;

&lt;p&gt;When investigating a regression, I would not immediately sort by “largest file.”&lt;/p&gt;

&lt;p&gt;The largest package in the application may have existed for years and have nothing to do with the current change.&lt;/p&gt;

&lt;p&gt;Instead, start with the largest &lt;strong&gt;absolute deltas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A package moving from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;600 KB → 603 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is probably less interesting than a package moving from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20 KB → 170 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though the first package remains much larger overall.&lt;/p&gt;

&lt;p&gt;Likewise, inspect decreases.&lt;/p&gt;

&lt;p&gt;Suppose a dependency upgrade adds 200 KB but removes another 180 KB library.&lt;/p&gt;

&lt;p&gt;Looking only at additions can make the release look much worse than the actual net change.&lt;/p&gt;

&lt;p&gt;A comparison should help you understand the complete movement of bytes rather than produce a blacklist of whatever currently occupies the most space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trace the regression down one level at a time
&lt;/h2&gt;

&lt;p&gt;A useful workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total bundle delta
        ↓
Category delta
        ↓
Package delta
        ↓
File delta
        ↓
Source / feature being reviewed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine a pull request increases the bundle by 240 KB.&lt;/p&gt;

&lt;p&gt;The category view says nearly all of it came from dependencies.&lt;/p&gt;

&lt;p&gt;One newly added package accounts for 210 KB.&lt;/p&gt;

&lt;p&gt;Inside that package, two modules contribute most of the generated output.&lt;/p&gt;

&lt;p&gt;Now the review can ask concrete questions:&lt;/p&gt;

&lt;p&gt;Did we intentionally add this dependency?&lt;/p&gt;

&lt;p&gt;Are we importing the broad package entry point?&lt;/p&gt;

&lt;p&gt;Is a smaller subpath available?&lt;/p&gt;

&lt;p&gt;Did the package replace code elsewhere?&lt;/p&gt;

&lt;p&gt;Is the cost justified by the feature?&lt;/p&gt;

&lt;p&gt;That is a much better discussion than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can we somehow reduce bundle size?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bundle analysis should produce investigation paths, not automatic architectural verdicts.&lt;/p&gt;

&lt;p&gt;A large dependency may be entirely justified. The useful part is knowing that the cost exists and where it came from. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical pull-request workflow
&lt;/h2&gt;

&lt;p&gt;The workflow I would use for a meaningful change is straightforward.&lt;/p&gt;

&lt;p&gt;Generate a production baseline from the target branch or last accepted release.&lt;/p&gt;

&lt;p&gt;Generate the candidate using equivalent build settings.&lt;/p&gt;

&lt;p&gt;Keep each bundle beside its matching source map.&lt;/p&gt;

&lt;p&gt;Open both build pairs in Compare mode.&lt;/p&gt;

&lt;p&gt;Review the total and category changes first.&lt;/p&gt;

&lt;p&gt;Then filter to changed, added, or removed entries.&lt;/p&gt;

&lt;p&gt;Inspect the largest absolute package and file deltas.&lt;/p&gt;

&lt;p&gt;Search for the feature, package, or source path involved in the pull request.&lt;/p&gt;

&lt;p&gt;Finally, export the result if the explanation belongs in the PR or release record. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The goal is to turn:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bundle increased by 312 KB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;into something closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The feature added application code, introduced package X, removed package Y, and most of the net increase comes from two specific source groups.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That explanation is reviewable.&lt;/p&gt;

&lt;p&gt;The raw total is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the baseline meaningful
&lt;/h2&gt;

&lt;p&gt;A comparison is only as useful as the baseline.&lt;/p&gt;

&lt;p&gt;For pull requests, &lt;code&gt;main&lt;/code&gt; or the target branch is often the obvious baseline.&lt;/p&gt;

&lt;p&gt;For release engineering, the previous accepted production release may be more useful.&lt;/p&gt;

&lt;p&gt;For a large dependency migration, you might intentionally preserve the pre-migration build as a long-lived reference.&lt;/p&gt;

&lt;p&gt;What matters is that the baseline represents a state the team understands.&lt;/p&gt;

&lt;p&gt;If the baseline itself contains several unrelated pending changes, the comparison becomes harder to interpret.&lt;/p&gt;

&lt;p&gt;Bundle-size analysis works best when the question is narrow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What did this release change relative to the state we intended to compare against?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Export the explanation, not the source map
&lt;/h2&gt;

&lt;p&gt;Source maps can be sensitive engineering artifacts.&lt;/p&gt;

&lt;p&gt;They may contain source paths and may include &lt;code&gt;sourcesContent&lt;/code&gt;, which can contain original source text.&lt;/p&gt;

&lt;p&gt;That is one reason Sight performs its analysis locally in the browser: the selected production bundle and source map are not uploaded to Bundle Drop. The same local-only boundary applies to the analysis and report generation. (&lt;a href="https://bundledrop.app/sight" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The normalized result can then be exported as evidence.&lt;/p&gt;

&lt;p&gt;Sight currently supports local reporting formats including PDF, standalone HTML, Markdown, normalized JSON, and relevant image exports. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That can be useful for attaching an explanation to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a pull request
a release review
a dependency migration
an architecture discussion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But exported reports can still reveal package names, source paths, and size information.&lt;/p&gt;

&lt;p&gt;Local analysis reduces the source-code exposure boundary.&lt;/p&gt;

&lt;p&gt;It does not make every resulting report safe to publish publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not turn bundle bytes into a performance score
&lt;/h2&gt;

&lt;p&gt;Bundle size is useful evidence.&lt;/p&gt;

&lt;p&gt;It is not a complete performance model.&lt;/p&gt;

&lt;p&gt;A smaller module can execute much more expensive work than a larger one.&lt;/p&gt;

&lt;p&gt;Generated-byte attribution does not directly measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startup CPU time
memory pressure
rendering cost
runtime network behavior
native binary size
compressed transfer size
Hermes bytecode size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metro configuration, Babel transforms, platform resolution, and tree-shaking behavior can also change generated output. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So if the application starts slowly, use runtime and native profiling tools.&lt;/p&gt;

&lt;p&gt;If the question is dependency graph structure or transformed modules, Expo Atlas may be the more appropriate tool.&lt;/p&gt;

&lt;p&gt;If the question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where did these generated JavaScript bytes come from, and what changed since the previous build?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;then source-map-based comparison is a good fit. (&lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyze and Compare solve different problems
&lt;/h2&gt;

&lt;p&gt;This is the distinction I find most useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze&lt;/strong&gt; asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is inside this build?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is good for understanding composition, large packages, large application files, and unattributed output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare&lt;/strong&gt; asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What changed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is better when a pull request, dependency upgrade, or release unexpectedly moves the total.&lt;/p&gt;

&lt;p&gt;Bundle Drop Sight exposes both workflows, but they are intentionally separate because a current-state inspection and a regression investigation are different engineering tasks. (&lt;a href="https://bundledrop.app/sight" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Sight is available here: &lt;a href="https://bundledrop.app/sight?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Open Bundle Drop Sight&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is free, requires no account, and analyzes the selected bundle and source map locally in the browser. (&lt;a href="https://bundledrop.app/sight" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;I originally built Sight while working on Bundle Drop’s React Native and Expo release tooling, but the analyzer does not require Bundle Drop OTA and is useful independently of it.&lt;/p&gt;

&lt;p&gt;The technical source explaining attribution, matching, comparison, exports, privacy, and limitations is: &lt;a href="https://bundledrop.app/resources/react-native-bundle-analysis?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;How to Analyze a React Native Bundle with Source Maps&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The useful goal is not automatically making every bundle smaller.&lt;/p&gt;

&lt;p&gt;It is being able to explain &lt;strong&gt;why it changed&lt;/strong&gt; before that change becomes a release surprise.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>performance</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Managed vs Self-Hosted React Native OTA: Compare the Ownership Boundary</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Wed, 26 Aug 2026 23:19:55 +0000</pubDate>
      <link>https://dev.to/gfean/managed-vs-self-hosted-react-native-ota-compare-the-ownership-boundary-48li</link>
      <guid>https://dev.to/gfean/managed-vs-self-hosted-react-native-ota-compare-the-ownership-boundary-48li</guid>
      <description>&lt;p&gt;“Managed or self-hosted?” sounds like an infrastructure question.&lt;/p&gt;

&lt;p&gt;For React Native OTA updates, it is really an &lt;strong&gt;ownership question&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both approaches can deliver compatible JavaScript and bundled assets to installed applications.&lt;/p&gt;

&lt;p&gt;Both can support channels.&lt;/p&gt;

&lt;p&gt;Both can support rollouts.&lt;/p&gt;

&lt;p&gt;Both can support rollback.&lt;/p&gt;

&lt;p&gt;Both can use object storage and CDNs.&lt;/p&gt;

&lt;p&gt;The important difference is not where the JavaScript file physically lives.&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is responsible when the release path stops behaving correctly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A managed service puts much of the OTA platform operation behind a provider boundary.&lt;/p&gt;

&lt;p&gt;A self-hosted system moves that responsibility into your organization.&lt;/p&gt;

&lt;p&gt;The safety requirements do not disappear.&lt;/p&gt;

&lt;p&gt;The ownership boundary moves. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The mobile team still owns compatibility either way
&lt;/h2&gt;

&lt;p&gt;One responsibility cannot really be outsourced:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deciding whether a release is compatible with the native binary already installed on the device.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React Native OTA can replace JavaScript and bundled assets.&lt;/p&gt;

&lt;p&gt;It cannot add a native module, permission, entitlement, engine, or compiled capability to an application that users already installed.&lt;/p&gt;

&lt;p&gt;So whether the OTA infrastructure is managed or self-hosted, somebody on the application side still needs to decide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this binary safely execute this update?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bundle Drop expresses this with runtime gating, but the principle is provider-independent.&lt;/p&gt;

&lt;p&gt;The current SDK only delivers updates that match the installed app’s native/runtime identity, and native changes still require another App Store or Play Store build. (&lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Self-hosting gives you more control over how that compatibility rule is implemented.&lt;/p&gt;

&lt;p&gt;It does not remove the need to define the rule correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare responsibilities, not labels
&lt;/h2&gt;

&lt;p&gt;“Self-hosted” covers a wide range of systems.&lt;/p&gt;

&lt;p&gt;One company may run an existing open-source OTA server on infrastructure it already knows well.&lt;/p&gt;

&lt;p&gt;Another may build a custom control plane around S3, a database, CDN infrastructure, signing keys, and an internal deployment platform.&lt;/p&gt;

&lt;p&gt;Likewise, “managed” can mean different service boundaries depending on the provider.&lt;/p&gt;

&lt;p&gt;The more useful comparison is the responsibility map.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Managed OTA&lt;/th&gt;
&lt;th&gt;Self-hosted OTA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native integration&lt;/td&gt;
&lt;td&gt;App team&lt;/td&gt;
&lt;td&gt;App team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility policy&lt;/td&gt;
&lt;td&gt;App team&lt;/td&gt;
&lt;td&gt;App team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control-plane availability&lt;/td&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;Your organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact infrastructure&lt;/td&gt;
&lt;td&gt;Provider operates it&lt;/td&gt;
&lt;td&gt;Your organization operates it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server upgrades&lt;/td&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;Your organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backups and restores&lt;/td&gt;
&lt;td&gt;Provider contract&lt;/td&gt;
&lt;td&gt;Your organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service security patching&lt;/td&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;Your organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom topology&lt;/td&gt;
&lt;td&gt;Limited to supported model&lt;/td&gt;
&lt;td&gt;Potentially very flexible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incident response&lt;/td&gt;
&lt;td&gt;Shared boundary&lt;/td&gt;
&lt;td&gt;Internal ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a scorecard.&lt;/p&gt;

&lt;p&gt;A small mobile team can benefit enormously from standardized managed behavior.&lt;/p&gt;

&lt;p&gt;A regulated organization with an established platform team may legitimately need infrastructure inside a particular region, network boundary, or internal identity system.&lt;/p&gt;

&lt;p&gt;The question is which responsibility boundary fits the organization you actually have. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile clients make self-hosting different from running a normal web API
&lt;/h2&gt;

&lt;p&gt;There is a complication with OTA infrastructure that is easy to underestimate.&lt;/p&gt;

&lt;p&gt;Your clients cannot all be upgraded when the server changes.&lt;/p&gt;

&lt;p&gt;A backend team can often deploy a coordinated frontend/backend change within a controlled window.&lt;/p&gt;

&lt;p&gt;A mobile fleet is different.&lt;/p&gt;

&lt;p&gt;Users keep old store binaries.&lt;/p&gt;

&lt;p&gt;Some update immediately.&lt;/p&gt;

&lt;p&gt;Some wait weeks.&lt;/p&gt;

&lt;p&gt;Some managed devices wait much longer.&lt;/p&gt;

&lt;p&gt;That means a self-hosted OTA server may need to preserve protocol behavior for several generations of installed clients at the same time.&lt;/p&gt;

&lt;p&gt;A server migration therefore needs questions such as:&lt;/p&gt;

&lt;p&gt;Which old binaries still exist?&lt;/p&gt;

&lt;p&gt;Which manifest or download contracts do they understand?&lt;/p&gt;

&lt;p&gt;How long must those contracts stay available?&lt;/p&gt;

&lt;p&gt;What happens if the database schema changes?&lt;/p&gt;

&lt;p&gt;Can a new server version still support rollback for an older client?&lt;/p&gt;

&lt;p&gt;The canonical Bundle Drop comparison explicitly calls this out: in a self-hosted model, the team may be responsible for coordinating client and server protocol compatibility while old store binaries remain active. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This is one of the biggest differences between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I can run this server.”&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;“We can operate this release system for the next three years.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Object storage is not an OTA platform
&lt;/h2&gt;

&lt;p&gt;The easiest part of an OTA system is often putting an artifact somewhere downloadable.&lt;/p&gt;

&lt;p&gt;A bucket plus CDN can solve storage and transfer.&lt;/p&gt;

&lt;p&gt;That is not the complete release path.&lt;/p&gt;

&lt;p&gt;A production OTA system also needs deterministic release selection, compatibility enforcement, artifact integrity, lifecycle management, rollback behavior, retries, safe activation, telemetry, access control, and incident recovery. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Consider what happens when:&lt;/p&gt;

&lt;p&gt;the database is healthy but object storage is degraded;&lt;/p&gt;

&lt;p&gt;the artifact exists but the signing credential expired;&lt;/p&gt;

&lt;p&gt;a device downloads only part of a bundle;&lt;/p&gt;

&lt;p&gt;the control plane chooses the wrong runtime line;&lt;/p&gt;

&lt;p&gt;a CDN still serves a release that was revoked;&lt;/p&gt;

&lt;p&gt;or the service restores metadata from backup without restoring the matching artifacts.&lt;/p&gt;

&lt;p&gt;Those failures are not theoretical implementation details.&lt;/p&gt;

&lt;p&gt;They are part of the service you are choosing to own.&lt;/p&gt;

&lt;p&gt;A self-hosted design gives you direct control over how they are handled.&lt;/p&gt;

&lt;p&gt;It also makes your team responsible for testing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release controls need an operator behind them
&lt;/h2&gt;

&lt;p&gt;Channels, staged rollout, targeting, and rollback look like product features.&lt;/p&gt;

&lt;p&gt;Operationally, each one is a state machine.&lt;/p&gt;

&lt;p&gt;Take staged rollout.&lt;/p&gt;

&lt;p&gt;A percentage value by itself is not enough.&lt;/p&gt;

&lt;p&gt;The system needs deterministic eligibility.&lt;/p&gt;

&lt;p&gt;It needs to know what happens when a device falls outside the newest release’s percentage.&lt;/p&gt;

&lt;p&gt;It needs to handle paused releases, targeting rules, revoked artifacts, and several runtime versions on one channel.&lt;/p&gt;

&lt;p&gt;Rollback also has more than one time horizon.&lt;/p&gt;

&lt;p&gt;Changing server-side release selection helps devices when they next successfully check for an update.&lt;/p&gt;

&lt;p&gt;It does not automatically rescue a device that already installed broken JavaScript and now fails before it can make another successful server request.&lt;/p&gt;

&lt;p&gt;That requires local recovery behavior and a known-good fallback.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s current public model separates those two concerns: server-side release control and local rollback safety both exist, while the complete embedded/native bundle remains part of the recovery model. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;In a managed system, the provider implements and operates the release-selection machinery.&lt;/p&gt;

&lt;p&gt;In a self-hosted system, your organization owns the correctness of that machinery.&lt;/p&gt;

&lt;p&gt;That is often where the real cost of customization appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch delivery adds another operational path
&lt;/h2&gt;

&lt;p&gt;Patch-sized downloads are attractive because many OTA releases change only a small fraction of the bundle.&lt;/p&gt;

&lt;p&gt;But patches introduce another condition:&lt;/p&gt;

&lt;p&gt;the device needs a suitable starting state.&lt;/p&gt;

&lt;p&gt;A production fleet will contain installations with different histories.&lt;/p&gt;

&lt;p&gt;Some users missed a release.&lt;/p&gt;

&lt;p&gt;Some installed a fresh binary.&lt;/p&gt;

&lt;p&gt;Some rolled back.&lt;/p&gt;

&lt;p&gt;Some were outside an earlier rollout cohort.&lt;/p&gt;

&lt;p&gt;So an optimized patch route needs a defined fallback.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s current public SDK treats patch transport as an optimization: it reconstructs and verifies the complete target bundle, and if patch transport is unavailable or fails, it can fall back to the signed full ZIP. (&lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;A self-hosted implementation can use any transport design it wants.&lt;/p&gt;

&lt;p&gt;But somebody must own the failure semantics.&lt;/p&gt;

&lt;p&gt;What if the patch artifact was cleaned up?&lt;/p&gt;

&lt;p&gt;What if the source state does not match?&lt;/p&gt;

&lt;p&gt;What if reconstruction fails?&lt;/p&gt;

&lt;p&gt;What if the patch generator and client support different algorithms?&lt;/p&gt;

&lt;p&gt;The bandwidth optimization becomes another production subsystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability is part of the OTA platform
&lt;/h2&gt;

&lt;p&gt;OTA creates a debugging problem that normal store releases do not have.&lt;/p&gt;

&lt;p&gt;Two devices can report the same native application version while running different JavaScript releases.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App version: 4.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is no longer enough to identify the running code.&lt;/p&gt;

&lt;p&gt;You need the active OTA release identity as well.&lt;/p&gt;

&lt;p&gt;Bundle Drop currently exposes the active bundle identity and observability context specifically so error trackers can associate a crash with the correct source map and JavaScript release. Its docs recommend using the OTA bundle hash to connect runtime reports with the corresponding source map. (&lt;a href="https://bundledrop.app/docs/observability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;A self-hosted platform needs an equivalent model.&lt;/p&gt;

&lt;p&gt;You should know:&lt;/p&gt;

&lt;p&gt;Which release is running?&lt;/p&gt;

&lt;p&gt;Which runtime line does it belong to?&lt;/p&gt;

&lt;p&gt;Which channel selected it?&lt;/p&gt;

&lt;p&gt;Did the download succeed?&lt;/p&gt;

&lt;p&gt;Did verification succeed?&lt;/p&gt;

&lt;p&gt;Was the candidate staged but not yet active?&lt;/p&gt;

&lt;p&gt;Did a rollback happen?&lt;/p&gt;

&lt;p&gt;Without that information, operating your own infrastructure may give you more server visibility while making application incidents harder to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access control and audit belong in the design too
&lt;/h2&gt;

&lt;p&gt;Once OTA becomes a production release mechanism, publishing is a privileged operation.&lt;/p&gt;

&lt;p&gt;So is rollback.&lt;/p&gt;

&lt;p&gt;So is revocation.&lt;/p&gt;

&lt;p&gt;So is deleting artifacts.&lt;/p&gt;

&lt;p&gt;So is changing a rollout from 5% to 100%.&lt;/p&gt;

&lt;p&gt;A self-hosted platform therefore needs more than authentication.&lt;/p&gt;

&lt;p&gt;It needs a clear authorization model.&lt;/p&gt;

&lt;p&gt;Read access and release-changing access should not automatically be equivalent.&lt;/p&gt;

&lt;p&gt;Operator actions should be attributable.&lt;/p&gt;

&lt;p&gt;Audit history needs retention.&lt;/p&gt;

&lt;p&gt;Credentials need rotation.&lt;/p&gt;

&lt;p&gt;CI credentials need narrower permissions than human administrators where possible.&lt;/p&gt;

&lt;p&gt;These concerns may not matter during the first internal proof of concept.&lt;/p&gt;

&lt;p&gt;They matter a lot after OTA becomes part of the normal production-release path. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source does not mean zero operating cost
&lt;/h2&gt;

&lt;p&gt;One of the weakest comparisons between managed and self-hosted infrastructure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Managed service: $X / month
Self-hosted software: $0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That measures software licensing.&lt;/p&gt;

&lt;p&gt;It does not measure operating cost.&lt;/p&gt;

&lt;p&gt;Self-hosting may still be the cheaper choice for a team that already has mature infrastructure for:&lt;/p&gt;

&lt;p&gt;deployment;&lt;/p&gt;

&lt;p&gt;identity;&lt;/p&gt;

&lt;p&gt;object storage;&lt;/p&gt;

&lt;p&gt;databases;&lt;/p&gt;

&lt;p&gt;monitoring;&lt;/p&gt;

&lt;p&gt;backups;&lt;/p&gt;

&lt;p&gt;on-call;&lt;/p&gt;

&lt;p&gt;incident response;&lt;/p&gt;

&lt;p&gt;security patching.&lt;/p&gt;

&lt;p&gt;For that organization, the marginal cost of another service may be small.&lt;/p&gt;

&lt;p&gt;For a mobile team without that platform foundation, the engineering time required to create and maintain the same safety envelope can be much larger than the infrastructure bill.&lt;/p&gt;

&lt;p&gt;The inverse is also true.&lt;/p&gt;

&lt;p&gt;A managed service should not automatically be assumed to be cheaper.&lt;/p&gt;

&lt;p&gt;Provider pricing, supported regions, limits, migration options, and organizational constraints all matter.&lt;/p&gt;

&lt;p&gt;The useful comparison is &lt;strong&gt;total operating cost over the lifetime of the system&lt;/strong&gt;, not subscription price versus server price. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-to-integrate is not time-to-operate
&lt;/h2&gt;

&lt;p&gt;A proof of concept answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this app download and launch an update?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Production readiness asks much more.&lt;/p&gt;

&lt;p&gt;Can you upgrade the server without breaking old store binaries?&lt;/p&gt;

&lt;p&gt;Can you restore both metadata and artifacts?&lt;/p&gt;

&lt;p&gt;Can you rotate credentials?&lt;/p&gt;

&lt;p&gt;Can you detect a release-selection incident?&lt;/p&gt;

&lt;p&gt;Can you recover from object-storage failure?&lt;/p&gt;

&lt;p&gt;Can you identify which JavaScript release caused a crash?&lt;/p&gt;

&lt;p&gt;Can you roll back while part of the fleet is offline?&lt;/p&gt;

&lt;p&gt;Can the embedded bundle still start if the OTA service is unavailable?&lt;/p&gt;

&lt;p&gt;A managed service does not remove all of those questions.&lt;/p&gt;

&lt;p&gt;It changes who owns large parts of the answer.&lt;/p&gt;

&lt;p&gt;The app team still needs native integration, runtime policy, rollout practice, source-map handling, incident procedures, and SDK upgrades.&lt;/p&gt;

&lt;p&gt;Self-hosting adds platform operation on top of those responsibilities. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  When self-hosting is the stronger fit
&lt;/h2&gt;

&lt;p&gt;Self-hosting starts to make sense when control is an architectural requirement rather than a preference.&lt;/p&gt;

&lt;p&gt;For example, an organization may have hard data-locality requirements.&lt;/p&gt;

&lt;p&gt;It may need a particular isolated network topology.&lt;/p&gt;

&lt;p&gt;It may already have an internal identity and release-control platform that every production service must use.&lt;/p&gt;

&lt;p&gt;It may need custom protocol or rollout behavior that available managed services cannot provide.&lt;/p&gt;

&lt;p&gt;And—critically—it may already have a named platform team willing to own the service.&lt;/p&gt;

&lt;p&gt;That last part matters.&lt;/p&gt;

&lt;p&gt;“I want control” is not the same requirement as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This team accepts availability, security, backups, upgrades, protocol compatibility, and on-call ownership.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When managed OTA is the stronger fit
&lt;/h2&gt;

&lt;p&gt;A managed service is often the better fit when the mobile team wants to own &lt;strong&gt;release policy&lt;/strong&gt; without owning another production platform.&lt;/p&gt;

&lt;p&gt;The team still decides:&lt;/p&gt;

&lt;p&gt;which native changes require a new binary;&lt;/p&gt;

&lt;p&gt;which runtime is compatible;&lt;/p&gt;

&lt;p&gt;which release should go to production;&lt;/p&gt;

&lt;p&gt;which audience should receive it;&lt;/p&gt;

&lt;p&gt;when to pause or roll back.&lt;/p&gt;

&lt;p&gt;But the team does not also need to operate the underlying OTA service.&lt;/p&gt;

&lt;p&gt;That trade can be especially valuable when the mobile team is small and the release controls offered by the provider already fit the application’s requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six questions that usually make the decision clearer
&lt;/h2&gt;

&lt;p&gt;Before choosing, assign an explicit owner to these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who gets paged when update checks or artifact downloads fail?&lt;/li&gt;
&lt;li&gt;Who upgrades the OTA service and proves old store binaries still work afterward?&lt;/li&gt;
&lt;li&gt;Who restores release metadata and artifacts after a failure?&lt;/li&gt;
&lt;li&gt;Who rotates delivery or signing credentials without breaking installed apps?&lt;/li&gt;
&lt;li&gt;Who validates rollout, rollback, and fallback behavior after React Native, Expo, iOS, or Android changes?&lt;/li&gt;
&lt;li&gt;Which requirements truly need custom infrastructure rather than simply feeling nicer to control?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answers are obvious, the deployment choice usually becomes easier.&lt;/p&gt;

&lt;p&gt;If nobody owns them, “self-hosted” is currently describing a topology rather than an operating model. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Bundle Drop fits today
&lt;/h2&gt;

&lt;p&gt;Disclosure: I built Bundle Drop.&lt;/p&gt;

&lt;p&gt;Today, Bundle Drop should be evaluated as a &lt;strong&gt;managed OTA service&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Its public SDK currently documents Expo and bare React Native support, runtime gating, channel-based distribution, targeted rollouts, rollback safety, hybrid patch/full-bundle delivery, observability metadata, and CI/CD publishing. (&lt;a href="https://www.npmjs.com/package/%40gfean/react-native-bundle-drop" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Self-hosting is part of the project’s longer-term direction.&lt;/p&gt;

&lt;p&gt;There is &lt;strong&gt;no announced availability date and no currently supported self-hosted Bundle Drop deployment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So a team with a hard self-hosting requirement today should evaluate systems that satisfy that requirement today rather than planning around an unannounced future capability. (&lt;a href="https://bundledrop.app/resources/managed-vs-self-hosted-ota" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That limitation is important.&lt;/p&gt;

&lt;p&gt;Infrastructure ownership is an architectural constraint.&lt;/p&gt;

&lt;p&gt;It should not depend on roadmap assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the boundary your team can sustain
&lt;/h2&gt;

&lt;p&gt;Managed and self-hosted OTA can both be reliable.&lt;/p&gt;

&lt;p&gt;They move responsibility to different places.&lt;/p&gt;

&lt;p&gt;Managed delivery trades some infrastructure control for an operated service boundary.&lt;/p&gt;

&lt;p&gt;Self-hosting trades provider dependence for greater operational responsibility and customization.&lt;/p&gt;

&lt;p&gt;The right choice is not the architecture that is easiest to demo.&lt;/p&gt;

&lt;p&gt;It is the one your organization can still understand, maintain, secure, and recover during an incident years after the first successful OTA update.&lt;/p&gt;

&lt;p&gt;Originally published on Bundle Drop Resources:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://bundledrop.app/resources/managed-vs-self-hosted-ota&lt;/code&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>devops</category>
      <category>mobile</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Runtime Versions: The Compatibility Boundary for React Native OTA Updates</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Wed, 19 Aug 2026 23:50:20 +0000</pubDate>
      <link>https://dev.to/gfean/runtime-versions-the-compatibility-boundary-for-react-native-ota-updates-3p6a</link>
      <guid>https://dev.to/gfean/runtime-versions-the-compatibility-boundary-for-react-native-ota-updates-3p6a</guid>
      <description>&lt;p&gt;A React Native application is not one artifact.&lt;/p&gt;

&lt;p&gt;The binary users install from the App Store or Play Store contains native code, native dependencies, platform configuration, a JavaScript engine, and an embedded JavaScript bundle.&lt;/p&gt;

&lt;p&gt;An OTA release replaces only part of that system: the JavaScript bundle and compatible bundled assets.&lt;/p&gt;

&lt;p&gt;That creates the most important safety question in any React Native OTA system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can this new JavaScript safely execute inside the native binary already installed on the device?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A runtime version exists to answer that question.&lt;/p&gt;

&lt;p&gt;In Bundle Drop, &lt;code&gt;runtimeVersion&lt;/code&gt; names the native compatibility boundary. An OTA bundle is only eligible when its platform and runtime version match the installed binary. It is not a “newer than” comparison and it is not simply another app version.&lt;/p&gt;

&lt;h2&gt;
  
  
  App version and runtime version solve different problems
&lt;/h2&gt;

&lt;p&gt;It is tempting to reuse the application version for everything.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App version: 4.2.0
Runtime version: 4.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may be convenient for a particular release strategy, but the two concepts are different.&lt;/p&gt;

&lt;p&gt;The application version answers something like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which product release is this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The runtime version answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which native interface can this JavaScript safely run against?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multiple JavaScript releases can therefore exist inside the same native runtime.&lt;/p&gt;

&lt;p&gt;And two native app releases can sometimes share a runtime if their native compatibility surface is unchanged.&lt;/p&gt;

&lt;p&gt;Bundle Drop records both kinds of identity separately; its CI result metadata, for example, includes the application version and &lt;code&gt;runtimeVersion&lt;/code&gt; as distinct fields.&lt;/p&gt;

&lt;p&gt;That separation matters because otherwise the OTA compatibility model becomes coupled to product-versioning decisions that may have nothing to do with native safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the runtime when the native boundary did not change
&lt;/h2&gt;

&lt;p&gt;Suppose your current configuration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runtimeVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.2-native-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.2-native-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You fix some TypeScript logic.&lt;/p&gt;

&lt;p&gt;Or change copy.&lt;/p&gt;

&lt;p&gt;Or adjust styling.&lt;/p&gt;

&lt;p&gt;Or add a screen built entirely from native capabilities that already exist in the installed binary.&lt;/p&gt;

&lt;p&gt;Those changes do not necessarily create a new compatibility boundary.&lt;/p&gt;

&lt;p&gt;The existing binaries already contain what the new JavaScript needs.&lt;/p&gt;

&lt;p&gt;So the runtime can remain unchanged.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s current public model explicitly recommends keeping a platform’s runtime literal stable for OTA-compatible JavaScript and asset changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bump the runtime when JavaScript can depend on new native state
&lt;/h2&gt;

&lt;p&gt;Now imagine adding a new iOS native module.&lt;/p&gt;

&lt;p&gt;The next JavaScript release imports that module.&lt;/p&gt;

&lt;p&gt;Older iOS binaries cannot safely run the new bundle because the native module simply does not exist inside them.&lt;/p&gt;

&lt;p&gt;Changing the OTA runtime label does not magically install the module.&lt;/p&gt;

&lt;p&gt;The correct sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;change the native code;&lt;/li&gt;
&lt;li&gt;create a new iOS runtime line;&lt;/li&gt;
&lt;li&gt;build and distribute a new iOS binary;&lt;/li&gt;
&lt;li&gt;publish OTA updates that depend on the new native capability only to that runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The old binary continues on its previous compatibility line.&lt;/p&gt;

&lt;p&gt;This same logic applies to changes such as native dependencies, permissions, generated native configuration, JavaScript-engine changes, architecture changes, or Expo config-plugin output that materially changes the native projects.&lt;/p&gt;

&lt;p&gt;A runtime version is a &lt;strong&gt;gate&lt;/strong&gt;, not an upgrade mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS and Android do not have to move together
&lt;/h2&gt;

&lt;p&gt;This is one of the most useful properties of a per-platform runtime model.&lt;/p&gt;

&lt;p&gt;Suppose you add a native dependency only on iOS.&lt;/p&gt;

&lt;p&gt;You might change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;runtimeVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.2-native-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.2-native-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;runtimeVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.2-native-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4.2-native-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android has not gained or lost any native capability.&lt;/p&gt;

&lt;p&gt;There is no compatibility reason to force it onto a new runtime line.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s canonical runtime article explicitly models these as independent lanes: an iOS native change can advance iOS while Android continues resolving the previous Android runtime.&lt;/p&gt;

&lt;p&gt;This is more than a configuration detail.&lt;/p&gt;

&lt;p&gt;React Native teams frequently ship platform-specific native changes.&lt;/p&gt;

&lt;p&gt;Forcing both platforms to advance every time one platform changes creates unnecessary runtime fragmentation and release bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old and new binaries normally coexist
&lt;/h2&gt;

&lt;p&gt;Publishing a new store binary does not mean every user installs it immediately.&lt;/p&gt;

&lt;p&gt;Some users update quickly.&lt;/p&gt;

&lt;p&gt;Some wait days or weeks.&lt;/p&gt;

&lt;p&gt;Managed devices can lag even longer.&lt;/p&gt;

&lt;p&gt;During that period, production can legitimately contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Older iOS binary
runtimeVersion = ios:4.2-native-1

Newer iOS binary
runtimeVersion = ios:4.2-native-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both binaries may continue checking the same production channel.&lt;/p&gt;

&lt;p&gt;The important part is that each installation resolves only updates that belong to its own runtime line.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s public resource explicitly describes channels containing several runtime lines while installed binaries receive only the exact compatible platform/runtime combination.&lt;/p&gt;

&lt;p&gt;This lets teams continue maintaining a safe OTA path for users who have not yet adopted the newest store release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime version and channel are different axes
&lt;/h2&gt;

&lt;p&gt;Another common mistake is treating channels as compatibility boundaries.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;A channel answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which release track should this app follow?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Examples might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;develop
beta
production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which native binaries can execute this update?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A production channel can therefore contain releases for multiple runtime lines.&lt;/p&gt;

&lt;p&gt;Likewise, two binaries on the same runtime could follow different channels.&lt;/p&gt;

&lt;p&gt;Bundle Drop documents these concepts separately because collapsing them creates awkward release models such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production-ios-v4-native2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where release track, platform and compatibility are all encoded into one string.&lt;/p&gt;

&lt;p&gt;Keeping the axes independent produces a cleaner model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channel: production
Platform: ios
Runtime: 4.2-native-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each value answers one question.&lt;/p&gt;

&lt;h2&gt;
  
  
  A runtime bump should follow a native change, not compensate for uncertainty
&lt;/h2&gt;

&lt;p&gt;It can be tempting to bump the runtime frequently “just to be safe.”&lt;/p&gt;

&lt;p&gt;That is safe in one narrow sense: fewer binaries will be considered compatible.&lt;/p&gt;

&lt;p&gt;But unnecessary runtime churn creates operational cost.&lt;/p&gt;

&lt;p&gt;Every new runtime line creates another compatibility branch that may coexist in production.&lt;/p&gt;

&lt;p&gt;Too coarse a boundary risks sending JavaScript to an incompatible binary.&lt;/p&gt;

&lt;p&gt;Too fine a boundary creates unnecessary fragmentation.&lt;/p&gt;

&lt;p&gt;The useful rule is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change the runtime when the native compatibility surface changed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do not use it merely as a release counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expo introduces an authority question
&lt;/h2&gt;

&lt;p&gt;Expo projects have another concept called &lt;code&gt;runtimeVersion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means an OTA integration needs to be explicit about which value is authoritative rather than silently mixing two independent runtime calculations.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s default Expo setup uses the literal per-platform runtime values from &lt;code&gt;bundle.drop.config.js&lt;/code&gt;, with those values embedded into the native build and used by uploads.&lt;/p&gt;

&lt;p&gt;For advanced Expo workflows, the current public SDK also supports opting into Expo runtime authority. In that mode, Bundle Drop requires the runtime identity from the corresponding native build rather than guessing it during OTA publication; CI documentation describes using a matching local or EAS build receipt for that path.&lt;/p&gt;

&lt;p&gt;The broader principle is useful even outside Bundle Drop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;one component should be authoritative for native compatibility.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the build system and OTA publisher derive that boundary differently, you have created exactly the ambiguity the runtime version was meant to remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime compatibility should be decided at release time
&lt;/h2&gt;

&lt;p&gt;You do not want to discover a compatibility mistake because a production device crashed.&lt;/p&gt;

&lt;p&gt;A useful release review asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did native code change?&lt;/li&gt;
&lt;li&gt;Did a native dependency change?&lt;/li&gt;
&lt;li&gt;Did permissions or generated native configuration change?&lt;/li&gt;
&lt;li&gt;Did the JavaScript engine or architecture change?&lt;/li&gt;
&lt;li&gt;Did an Expo config-plugin change alter native output?&lt;/li&gt;
&lt;li&gt;Did either platform change independently?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is yes, review that platform’s runtime before publishing the JavaScript release.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s setup and troubleshooting docs also surface runtime mismatches as a first-class delivery condition: an installation does not receive a bundle from a different runtime line.&lt;/p&gt;

&lt;p&gt;That fail-closed behavior is important.&lt;/p&gt;

&lt;p&gt;“No update available” is much safer than “run JavaScript against a native interface it was never built for.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime version is not enough by itself
&lt;/h2&gt;

&lt;p&gt;Compatibility only answers whether a bundle &lt;em&gt;can&lt;/em&gt; run.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;whether this is the correct release channel;&lt;/li&gt;
&lt;li&gt;whether the installation belongs in a targeting cohort;&lt;/li&gt;
&lt;li&gt;whether the staged-rollout percentage includes it;&lt;/li&gt;
&lt;li&gt;whether the bundle passed integrity verification;&lt;/li&gt;
&lt;li&gt;whether a previously installed update is healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those concerns need their own controls.&lt;/p&gt;

&lt;p&gt;This is why production OTA systems usually have several independent dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Platform
Runtime compatibility
Channel
Targeting
Rollout state
Release identity
Health / recovery state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runtime version is one of them, but it is arguably the first one.&lt;/p&gt;

&lt;p&gt;If the native compatibility decision is wrong, the rest of the release controls cannot make the JavaScript safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful mental model
&lt;/h2&gt;

&lt;p&gt;Think of a runtime version as a &lt;strong&gt;native contract identifier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The installed binary says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I provide native contract &lt;code&gt;ios:4.2-native-1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An OTA bundle says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I require native contract &lt;code&gt;ios:4.2-native-1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the contracts match, the release can continue through the rest of the eligibility process.&lt;/p&gt;

&lt;p&gt;If they do not, the OTA bundle is not a candidate for that binary.&lt;/p&gt;

&lt;p&gt;That model remains understandable whether the app has one store version or five active versions, whether iOS and Android move together or separately, and whether the release arrives through a development, beta, or production channel.&lt;/p&gt;

&lt;p&gt;That is exactly what a good compatibility boundary should do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The release rule to keep
&lt;/h2&gt;

&lt;p&gt;OTA lets React Native teams move compatible JavaScript and assets independently from store binaries.&lt;/p&gt;

&lt;p&gt;That independence only remains safe when the OTA system respects the native application underneath it.&lt;/p&gt;

&lt;p&gt;Keep the runtime when the installed binaries already provide everything the update needs.&lt;/p&gt;

&lt;p&gt;Create a new runtime line when the native interface changes.&lt;/p&gt;

&lt;p&gt;Move only the platforms that actually changed.&lt;/p&gt;

&lt;p&gt;And allow old and new runtime lines to coexist while users adopt the new store binary at their own pace.&lt;/p&gt;

&lt;p&gt;Bundle Drop implements that model with literal per-platform runtime versions by default, plus an explicit Expo-authority option for teams that need it. The principle itself is provider-independent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OTA compatibility should be explicit, stable and easy to explain during an incident.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bundledrop.app/resources/runtime-versions-compatibility-boundary" rel="noopener noreferrer"&gt;Originally published on Bundle Drop Resources&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Patch Delivery and Full-Bundle Reliability in React Native OTA</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Wed, 12 Aug 2026 08:21:05 +0000</pubDate>
      <link>https://dev.to/gfean/patch-delivery-and-full-bundle-reliability-in-react-native-ota-1l4c</link>
      <guid>https://dev.to/gfean/patch-delivery-and-full-bundle-reliability-in-react-native-ota-1l4c</guid>
      <description>&lt;p&gt;Most OTA releases do not replace most of an application.&lt;/p&gt;

&lt;p&gt;A production update might fix one conditional, adjust a request handler, or change a small group of bundled assets.&lt;/p&gt;

&lt;p&gt;Yet with a full-bundle-only delivery model, every eligible device still downloads the complete JavaScript and asset payload.&lt;/p&gt;

&lt;p&gt;That model is easy to reason about. It is also often wasteful.&lt;/p&gt;

&lt;p&gt;Patch delivery solves that efficiency problem by transferring a representation of what changed instead of sending the entire release again.&lt;/p&gt;

&lt;p&gt;But patches introduce another question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the device cannot safely use the patch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is where OTA delivery becomes more interesting than simply calculating the smallest possible download.&lt;/p&gt;

&lt;p&gt;A production system needs to optimize network transfer without making that optimization a requirement for reaching a valid release. That is the principle behind Bundle Drop’s hybrid approach: a patch can be the smaller route to a release, while the complete verified bundle remains available as the dependable route. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why patch delivery is useful
&lt;/h2&gt;

&lt;p&gt;Imagine a large React Native application where a release changes only a few lines of JavaScript.&lt;/p&gt;

&lt;p&gt;The generated production bundle still contains the rest of the application module graph, dependencies, framework code, and other unchanged content.&lt;/p&gt;

&lt;p&gt;If the update system always sends complete bundles, the amount of data transferred is tied more closely to the size of the application than to the size of the change.&lt;/p&gt;

&lt;p&gt;That matters because mobile networks are not uniform.&lt;/p&gt;

&lt;p&gt;Users can be on metered connections, high-latency networks, unstable Wi-Fi, or background execution windows that disappear before a large download completes.&lt;/p&gt;

&lt;p&gt;A smaller transfer generally spends less time exposed to those conditions and consumes less user bandwidth. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The advantage compounds when a team ships frequently.&lt;/p&gt;

&lt;p&gt;A sequence of small fixes does not need to mean repeatedly transferring the majority of the same JavaScript bundle to every device.&lt;/p&gt;

&lt;p&gt;When a client has the correct starting state, a patch can make the transfer cost look much more like the release itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patch has a dependency that a full bundle does not
&lt;/h2&gt;

&lt;p&gt;A complete release bundle is self-contained from the client’s perspective.&lt;/p&gt;

&lt;p&gt;If the platform and runtime compatibility rules match, the device can validate the artifact without reconstructing it from a previously installed OTA state.&lt;/p&gt;

&lt;p&gt;A patch is different.&lt;/p&gt;

&lt;p&gt;It describes a transition from an expected source state to a target state.&lt;/p&gt;

&lt;p&gt;That means the source matters.&lt;/p&gt;

&lt;p&gt;This sounds simple until you think about a real production fleet.&lt;/p&gt;

&lt;p&gt;Some users skipped an earlier update.&lt;/p&gt;

&lt;p&gt;Some installed the application recently and only have the bundle embedded in the App Store or Play Store binary.&lt;/p&gt;

&lt;p&gt;Some were offline during a rollout.&lt;/p&gt;

&lt;p&gt;Some received an earlier release and then recovered from it.&lt;/p&gt;

&lt;p&gt;Others may have been in different rollout or targeting cohorts.&lt;/p&gt;

&lt;p&gt;Production devices rarely form one perfect linear chain of versions. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;If a patch only works from version B to version C, a device currently running version A needs another route.&lt;/p&gt;

&lt;p&gt;That should not automatically become an application error.&lt;/p&gt;

&lt;p&gt;It can simply mean the optimized route is not appropriate for that device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat patching as an optimization, not a release prerequisite
&lt;/h2&gt;

&lt;p&gt;There are two obvious extremes.&lt;/p&gt;

&lt;p&gt;The first is to always send complete bundles.&lt;/p&gt;

&lt;p&gt;That gives every eligible device a self-contained artifact and keeps the delivery model simple.&lt;/p&gt;

&lt;p&gt;The downside is repeated over-transfer.&lt;/p&gt;

&lt;p&gt;The second extreme is to make patches mandatory.&lt;/p&gt;

&lt;p&gt;That minimizes transfer size when the expected source state exists, but every patch prerequisite becomes part of deployment correctness.&lt;/p&gt;

&lt;p&gt;Now you have to think about missing source versions, multiple patch paths, reconstruction failures, and devices whose local state does not match the expected chain.&lt;/p&gt;

&lt;p&gt;A better model is to separate the destination from the transport.&lt;/p&gt;

&lt;p&gt;The release is the complete verified result.&lt;/p&gt;

&lt;p&gt;The patch is one possible way of getting there.&lt;/p&gt;

&lt;p&gt;If the patch can safely produce that result, use it.&lt;/p&gt;

&lt;p&gt;If it cannot, fetch the complete release instead. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That gives the system an important invariant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every successful delivery path converges on the same verified release.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Release selection and transport are separate problems
&lt;/h2&gt;

&lt;p&gt;Another useful distinction is between deciding &lt;strong&gt;which release&lt;/strong&gt; a device should receive and deciding &lt;strong&gt;how&lt;/strong&gt; that release should be transferred.&lt;/p&gt;

&lt;p&gt;A device might first be evaluated against:&lt;/p&gt;

&lt;p&gt;channel, platform, runtime compatibility, rollout percentage, targeting rules, and release state.&lt;/p&gt;

&lt;p&gt;Only after the system selects an eligible release does patch availability matter.&lt;/p&gt;

&lt;p&gt;Two devices can therefore be eligible for the same release but receive it differently.&lt;/p&gt;

&lt;p&gt;One may already have a suitable source bundle and use an optimized patch.&lt;/p&gt;

&lt;p&gt;Another may need the complete bundle.&lt;/p&gt;

&lt;p&gt;Both should converge on the same target release.&lt;/p&gt;

&lt;p&gt;Bundle Drop’s current upload documentation reflects that separation: uploads become versioned OTA bundles, patch optimization is prepared afterward, and eligible installations can use the full bundle while patch work is still being prepared. If patching is not efficient, the system deliberately uses the full bundle instead. (&lt;a href="https://bundledrop.app/docs/uploading" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification matters more than transfer size
&lt;/h2&gt;

&lt;p&gt;A patch being small does not make it safe.&lt;/p&gt;

&lt;p&gt;A successful HTTP response does not make it safe either.&lt;/p&gt;

&lt;p&gt;The useful boundary is whether the intended complete target exists locally and has passed the validation required before staging.&lt;/p&gt;

&lt;p&gt;A patch can download correctly and still fail to reconstruct the expected result.&lt;/p&gt;

&lt;p&gt;A transfer can be interrupted.&lt;/p&gt;

&lt;p&gt;Local data can be incomplete.&lt;/p&gt;

&lt;p&gt;The device can run out of storage.&lt;/p&gt;

&lt;p&gt;The application can be terminated midway through work.&lt;/p&gt;

&lt;p&gt;Those are ordinary failure modes in mobile software.&lt;/p&gt;

&lt;p&gt;The system should have a binary outcome:&lt;/p&gt;

&lt;p&gt;the complete intended release is present and verified, or the current working bundle remains active.&lt;/p&gt;

&lt;p&gt;There should not be a halfway state where partially reconstructed content becomes executable. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This is also why transport logic should ideally stay below the application-facing API.&lt;/p&gt;

&lt;p&gt;Product code should be able to ask for an update and receive a staged release without branching into separate business logic for “patch mode” and “full bundle mode.”&lt;/p&gt;

&lt;p&gt;The transport is an implementation decision.&lt;/p&gt;

&lt;p&gt;The release state is the application concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-bundle fallback does not mean ignoring errors
&lt;/h2&gt;

&lt;p&gt;Fallback is not magic recovery.&lt;/p&gt;

&lt;p&gt;If the device has no usable connection, lacks storage, or cannot validate the complete artifact either, the update still cannot be staged.&lt;/p&gt;

&lt;p&gt;The correct behavior is to keep running the existing verified bundle and try again later according to the application’s update policy.&lt;/p&gt;

&lt;p&gt;The purpose of the full bundle is narrower:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a device should not be stranded just because the optimized patch route is unavailable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Transport fallback handles a delivery-path problem.&lt;/p&gt;

&lt;p&gt;It does not fix a bad release.&lt;/p&gt;

&lt;p&gt;If the target JavaScript itself is faulty, that becomes a rollback or recovery problem instead. The canonical Bundle Drop resource explicitly treats transport reliability and release correctness as separate concerns. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Progressive rollouts make mixed device states normal
&lt;/h2&gt;

&lt;p&gt;Percentage rollouts intentionally create different device states.&lt;/p&gt;

&lt;p&gt;During a staged rollout, some installations have the candidate release while others still run the previous one.&lt;/p&gt;

&lt;p&gt;Targeted release rules can create additional valid branches.&lt;/p&gt;

&lt;p&gt;Server-side rollbacks can change future release selection.&lt;/p&gt;

&lt;p&gt;Device-side recovery can move an installation back to an earlier verified bundle.&lt;/p&gt;

&lt;p&gt;In other words, the fleet becomes heterogeneous by design.&lt;/p&gt;

&lt;p&gt;That makes a delivery model that assumes every device is always exactly one release behind increasingly fragile.&lt;/p&gt;

&lt;p&gt;Patch optimization can still work well in that environment, but only when it is allowed to be conditional.&lt;/p&gt;

&lt;p&gt;Devices that have the expected starting state use the smaller route.&lt;/p&gt;

&lt;p&gt;Devices with different histories use the complete bundle.&lt;/p&gt;

&lt;p&gt;The rollout system can then focus on release health instead of maintaining one perfectly uniform transport chain. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters operationally
&lt;/h2&gt;

&lt;p&gt;It is tempting to evaluate an OTA system by the smallest patch it can produce.&lt;/p&gt;

&lt;p&gt;That is not a very useful production metric on its own.&lt;/p&gt;

&lt;p&gt;A better set of questions is:&lt;/p&gt;

&lt;p&gt;Can every compatible and eligible installation reach the release?&lt;/p&gt;

&lt;p&gt;What happens when the optimized path cannot be used?&lt;/p&gt;

&lt;p&gt;Does the current working bundle remain active after a failed delivery?&lt;/p&gt;

&lt;p&gt;Do patch and full-bundle paths converge on the same staged result?&lt;/p&gt;

&lt;p&gt;Are old and unusual device histories tested before broad rollout?&lt;/p&gt;

&lt;p&gt;Those questions are less exciting than “our update was only 30 KB,” but they are much closer to the real job of release engineering. The source article makes the same point: predictable reachability and safe activation matter more than a perfect efficiency ratio. (&lt;a href="https://bundledrop.app/resources/patch-delivery-full-bundle-reliability" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize both, force neither
&lt;/h2&gt;

&lt;p&gt;Patch delivery and full-bundle delivery are sometimes treated like opposing architectures.&lt;/p&gt;

&lt;p&gt;They do not have to be.&lt;/p&gt;

&lt;p&gt;A smaller transfer is useful because most releases change only part of an application.&lt;/p&gt;

&lt;p&gt;A complete release artifact is useful because production devices do not share one clean history.&lt;/p&gt;

&lt;p&gt;The two solve different problems.&lt;/p&gt;

&lt;p&gt;Patches optimize transport.&lt;/p&gt;

&lt;p&gt;Full bundles preserve an independent route to the release.&lt;/p&gt;

&lt;p&gt;Verification decides whether the result can be staged.&lt;/p&gt;

&lt;p&gt;That is the model Bundle Drop currently uses for React Native and Expo OTA delivery: the public SDK describes hybrid transport as maintaining full-bundle integrity while supported devices download patch-sized changes. (&lt;a href="https://github.com/GFean/react-native-bundle-drop" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The important idea is broader than any one platform:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bandwidth optimization should not become a new condition for deployment correctness.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The original technical resource for this article is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://bundledrop.app/resources/patch-delivery-full-bundle-reliability&lt;/code&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>devops</category>
    </item>
    <item>
      <title>Life After CodePush: Choosing an OTA Update Strategy for React Native in 2026</title>
      <dc:creator>George Fean</dc:creator>
      <pubDate>Tue, 04 Aug 2026 22:55:03 +0000</pubDate>
      <link>https://dev.to/gfean/life-after-codepush-choosing-an-ota-update-strategy-for-react-native-in-2026-56mi</link>
      <guid>https://dev.to/gfean/life-after-codepush-choosing-an-ota-update-strategy-for-react-native-in-2026-56mi</guid>
      <description>&lt;p&gt;Microsoft retired the hosted Visual Studio App Center service on March 31, 2025. That included the managed service many React Native teams knew through CodePush, although Microsoft also released a self-hostable CodePush server.&lt;/p&gt;

&lt;p&gt;The service retirement did not remove the underlying need. Teams still want to fix JavaScript bugs, adjust UI, update copy, and ship compatible asset changes without waiting for a new App Store or Play Store release.&lt;/p&gt;

&lt;p&gt;But choosing a replacement is not just a matter of finding a familiar &lt;code&gt;sync()&lt;/code&gt; call. An OTA system becomes part of your production runtime and your release process. The important questions are about compatibility, failure handling, rollout control, and how much of the system your team wants to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an OTA system actually has to manage
&lt;/h2&gt;

&lt;p&gt;An OTA update is not simply a file download. It is a decision about which code a particular installed binary can safely execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Native and runtime compatibility
&lt;/h3&gt;

&lt;p&gt;React Native lets you change JavaScript without rebuilding the native application, but only while that JavaScript remains compatible with the native code already on the device.&lt;/p&gt;

&lt;p&gt;Adding a native module, changing permissions, upgrading React Native, switching important native configuration, or changing a JavaScript engine can require a new binary. A production OTA system therefore needs a compatibility boundary—often called a runtime version—and must refuse to deliver an update across that boundary.&lt;/p&gt;

&lt;p&gt;This is one of the easiest details to overlook in a proof of concept and one of the most important details in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Channels and release tracks
&lt;/h3&gt;

&lt;p&gt;Most teams need more than one stream of updates. You may want production, beta, QA, internal, or customer-specific tracks.&lt;/p&gt;

&lt;p&gt;A channel is useful only if its relationship to builds and updates is explicit. Teams should know which installed binaries read from which channel, how an update moves between channels, and whether promoting a tested release preserves its identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe rollout strategies
&lt;/h3&gt;

&lt;p&gt;Sending an update to every eligible device at once is fast, but it gives you very little time to react.&lt;/p&gt;

&lt;p&gt;Percentage-based rollouts let you expose a release gradually. Targeted rollouts go further by limiting eligibility using properties such as plan, region, or an internal beta flag. These controls solve different problems: targeting defines who is eligible, while staging controls how much of that group receives the update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback and recovery
&lt;/h3&gt;

&lt;p&gt;A dashboard rollback is helpful, but device-side recovery matters too. Consider what happens when an update crashes before the app can check the server again.&lt;/p&gt;

&lt;p&gt;A robust design should define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when an update is considered healthy;&lt;/li&gt;
&lt;li&gt;how repeated launch failures are detected;&lt;/li&gt;
&lt;li&gt;whether the device can return to a last-known-good bundle;&lt;/li&gt;
&lt;li&gt;whether the embedded bundle remains a final fallback; and&lt;/li&gt;
&lt;li&gt;how a failed update is prevented from being retried on the same device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst time to discover that rollback depends on a successful JavaScript launch is after shipping JavaScript that cannot launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrity verification
&lt;/h3&gt;

&lt;p&gt;The client should verify that the downloaded artifact is exactly the artifact the service intended to deliver. That normally means secure transport plus cryptographic hashes or signatures, validation before activation, and an atomic transition so a partial install never becomes the active version.&lt;/p&gt;

&lt;p&gt;Integrity and compatibility are separate checks. A perfectly downloaded bundle can still be wrong for a device if it targets a different native runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Download size and bandwidth
&lt;/h3&gt;

&lt;p&gt;Some systems deliver a complete JavaScript bundle and asset set for every update. Others support patches or asset-level deltas.&lt;/p&gt;

&lt;p&gt;Patch delivery can reduce download size when a change is small, but it introduces another failure path. Ask whether the client reconstructs and verifies the complete target state and whether it can fall back to a full update when a patch is missing, unsupported, or invalid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational visibility
&lt;/h3&gt;

&lt;p&gt;At minimum, teams need to identify which release is running when an error occurs. Useful signals include bundle identifiers or hashes, adoption, download and install failures, rollout state, and source maps tied to the exact update.&lt;/p&gt;

&lt;p&gt;OTA tooling should complement your crash reporter, not hide update identity from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main approaches available
&lt;/h2&gt;

&lt;p&gt;There is no universal winner. The right choice depends on your application model, operational appetite, existing tooling, and risk tolerance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expo Updates and EAS Update
&lt;/h3&gt;

&lt;p&gt;The names are easy to blur together, so it helps to separate them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expo-updates&lt;/code&gt; is the client library that loads compatible updates.&lt;/li&gt;
&lt;li&gt;EAS Update is Expo's hosted cloud service for publishing and serving those updates.&lt;/li&gt;
&lt;li&gt;EAS Build is a separate service that produces app binaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/eas-update/introduction/" rel="noopener noreferrer"&gt;EAS Update&lt;/a&gt; supports Expo projects using Continuous Native Generation and existing React Native projects that install &lt;code&gt;expo-updates&lt;/code&gt;. It uses runtime versions for native compatibility, supports staged rollouts, provides deployment insights, and lets teams republish a previous stable update.&lt;/p&gt;

&lt;p&gt;For teams already standardized on Expo tooling, that integration can be a strong advantage. Evaluate the workflow conventions, update behavior, and pricing model against your needs rather than treating "uses React Native" as the only requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other managed OTA providers
&lt;/h3&gt;

&lt;p&gt;A managed provider can reduce the amount of infrastructure your team owns. The tradeoff is that you are adopting the provider's runtime model, release controls, pricing dimensions, and migration path.&lt;/p&gt;

&lt;p&gt;Compare providers on concrete behavior rather than broad feature labels. "Rollback," for example, might mean republishing an older release, switching a server-side pointer, or performing automatic recovery on the device. Those are not operationally identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-hosted or custom infrastructure
&lt;/h3&gt;

&lt;p&gt;Microsoft's &lt;a href="https://learn.microsoft.com/en-us/appcenter/retirement" rel="noopener noreferrer"&gt;App Center retirement guidance&lt;/a&gt; points CodePush users to a self-hostable server. Expo also documents self-hosting for the updates protocol. A custom system can offer maximum control over data, cost, and release policy.&lt;/p&gt;

&lt;p&gt;That control comes with ownership. Your team becomes responsible for artifact storage, signing, availability, client compatibility, rollout state, observability, abuse prevention, and incident response. Running an update server is not the same as safely operating an OTA lifecycle.&lt;/p&gt;

&lt;p&gt;Self-hosting is a reasonable choice when the constraints justify it and the team is prepared to maintain it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store-only releases
&lt;/h3&gt;

&lt;p&gt;Choosing not to use OTA is also valid.&lt;/p&gt;

&lt;p&gt;Store-only releases keep the delivery model simple and ensure every code change passes through the normal binary pipeline. The cost is slower delivery for small fixes and less control over when users install them.&lt;/p&gt;

&lt;p&gt;For apps with infrequent releases, strict review requirements, or a small operational team, that tradeoff may be appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions to ask before choosing
&lt;/h2&gt;

&lt;p&gt;A useful evaluation should be specific enough that two engineers can reach the same answer from the documentation or a test.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does it support our actual project shape: Expo, Expo Prebuild/CNG, EAS Build, or bare React Native?&lt;/li&gt;
&lt;li&gt;How is native compatibility represented, and who is responsible for changing the runtime version?&lt;/li&gt;
&lt;li&gt;Does every update download a full bundle, or can the client fetch a smaller patch when only part of the application changes?&lt;/li&gt;
&lt;li&gt;If patching fails, is there a verified full-bundle fallback?&lt;/li&gt;
&lt;li&gt;Are percentage rollouts available? Can targeting rules use application-defined user properties?&lt;/li&gt;
&lt;li&gt;What happens if an update repeatedly crashes before startup completes?&lt;/li&gt;
&lt;li&gt;Can the device recover without successfully running the new JavaScript?&lt;/li&gt;
&lt;li&gt;Are channels, promotions, and rollback operations auditable?&lt;/li&gt;
&lt;li&gt;Can source maps and release identifiers be connected to the existing error tracker?&lt;/li&gt;
&lt;li&gt;Is pricing tied to monthly active users, bandwidth, storage, builds, seats, or some combination?&lt;/li&gt;
&lt;li&gt;Can artifacts and release history be exported if the team later migrates away?&lt;/li&gt;
&lt;li&gt;Which changes still require a store build?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A small failure drill is often more revealing than another feature checklist: publish a deliberately broken update to a test channel, then observe how rollout, detection, rollback, and device recovery actually behave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built Bundle Drop
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I am the creator of Bundle Drop.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/GFean/react-native-bundle-drop" rel="noopener noreferrer"&gt;Bundle Drop&lt;/a&gt; because I wanted another managed option with an explicit compatibility boundary and a recovery model that remains useful when an update fails early.&lt;/p&gt;

&lt;p&gt;Its delivery model prepares a complete verified bundle and, when possible, a smaller patch. A device can download the patch when only part of the application changed instead of downloading the full update every time. The client reconstructs and verifies the complete target state before activation. If patch transport is unavailable or fails, it falls back to the signed full bundle.&lt;/p&gt;

&lt;p&gt;Bundle Drop also provides channels, staged rollouts, and targeting based on application-defined user properties. On-device health checks can return a failing update to the previous verified bundle, or to the binary's embedded bundle when there is no earlier OTA version.&lt;/p&gt;

&lt;p&gt;For Expo, the integration is designed for Prebuild/CNG projects and normal EAS Build workflows. EAS Build still creates the application binary; Bundle Drop handles the OTA layer after that binary is deployed. The same runtime-version model also supports bare React Native.&lt;/p&gt;

&lt;p&gt;The service's plans are not priced by monthly active users. That was an intentional choice because I wanted teams to evaluate rollout and storage needs without treating user growth itself as an OTA cost.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://bundledrop.app/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; describes the runtime and release model, and the &lt;a href="https://bundledrop.app/docs/expo" rel="noopener noreferrer"&gt;Expo integration guide&lt;/a&gt; covers config-plugin setup, CNG, EAS Build, and runtime authority. Bundle Drop is one option among several, and I would still recommend testing its failure behavior against your own release requirements before choosing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose for recovery, not just installation
&lt;/h2&gt;

&lt;p&gt;Installation is the beginning of an OTA system, not the hard part.&lt;/p&gt;

&lt;p&gt;Before committing to a provider or a self-hosted design, write down your native compatibility rule, rollout process, health signal, rollback path, observability requirements, and exit strategy. Then test those rules with real release builds.&lt;/p&gt;

&lt;p&gt;The best choice is the one your team can understand and operate during an incident—not merely the one with the shortest setup guide.&lt;/p&gt;

&lt;p&gt;If you are evaluating the post-CodePush landscape, I would be interested in hearing which operational requirements have mattered most to your team.&lt;/p&gt;

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