<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Joe Masilotti]]></title><description><![CDATA[I write about shipping mobile apps with Rails and running a solo business without burning out.]]></description><link>https://newsletter.masilotti.com</link><image><url>https://substackcdn.com/image/fetch/$s_!7JI_!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ddd4091-7614-483d-a47a-ab4a647abd44_1018x1018.jpeg</url><title>Joe Masilotti</title><link>https://newsletter.masilotti.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 17 Sep 2026 00:12:52 GMT</lastBuildDate><atom:link href="https://newsletter.masilotti.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Joe Masilotti]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[joemasilotti@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[joemasilotti@substack.com]]></itunes:email><itunes:name><![CDATA[Joe Masilotti]]></itunes:name></itunes:owner><itunes:author><![CDATA[Joe Masilotti]]></itunes:author><googleplay:owner><![CDATA[joemasilotti@substack.com]]></googleplay:owner><googleplay:email><![CDATA[joemasilotti@substack.com]]></googleplay:email><googleplay:author><![CDATA[Joe Masilotti]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Ruby Native apps are running on 100,000+ devices]]></title><description><![CDATA[And how Herb unlocks reliable and safe mobile app updates for Rails developers.]]></description><link>https://newsletter.masilotti.com/p/ruby-native-apps-are-running-on-100000</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/ruby-native-apps-are-running-on-100000</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Fri, 11 Sep 2026 13:08:42 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7fcc86c0-4044-4e55-b783-23e2a78fca2f_2400x1200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Bet you didn&#8217;t think you&#8217;d hear from me until January, yeah? Well, as of this writing, baby isn&#8217;t here yet, so I have a bit of extra time on my hands. And what timing, because I can share some of the most exciting (professional) news of my year: </span><strong><span>Ruby Native apps are now running on over 100,000 devices</span></strong><span>!</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://rubynative.com/showcase" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5hg2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 424w, https://substackcdn.com/image/fetch/$s_!5hg2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 848w, https://substackcdn.com/image/fetch/$s_!5hg2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 1272w, https://substackcdn.com/image/fetch/$s_!5hg2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5hg2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png" width="1456" height="463" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c46458c7-6332-4679-bae7-40a549225384_2622x834.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:463,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:163052,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://rubynative.com/showcase&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/215159145?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5hg2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 424w, https://substackcdn.com/image/fetch/$s_!5hg2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 848w, https://substackcdn.com/image/fetch/$s_!5hg2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 1272w, https://substackcdn.com/image/fetch/$s_!5hg2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc46458c7-6332-4679-bae7-40a549225384_2622x834.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">From Ruby Native&#8217;s showcase page, highlighting the apps built with the platform.</figcaption></figure></div><p><span>A huge nod to Equipe for making this possible. They recently </span><a href="https://rubynative.com/case-studies/equipe"><span>migrated their 10+ year-old Hotwire Native app to Ruby Native</span></a><span>. And with close to a million downloads over the last decade, they started contributing the majority of the active devices running Ruby Native apps.</span></p><p><span>This milestone feels a bit surreal for me. The first commit to Ruby Native was only in February of this year. Back then I just had a lofty dream of making mobile apps easier for Rails developers. But now? Real businesses are trusting Ruby Native. Equipe is no side project. It&#8217;s a profitable business supporting a huge portion of the equestrian events across Europe.</span></p><p><span>Right before I went on leave, I published a case study on how and why Equipe migrated. Check it out </span><a href="https://rubynative.com/case-studies/equipe"><span>on Ruby Native</span></a><span>. Jon, the founder of Equipe, went from having to hire contractors for every native update to running native builds and deploys himself. He even said &#8220;The app has never felt so good on Android. Love this.&#8221;</span></p><p><span>One of the big reasons Jon, a self-described &#8220;Rails guy,&#8221; is able to do this himself is thanks to </span><a href="https://herb-tools.dev/"><span>Herb</span></a><span>. Bundled with the Ruby Native gem is a </span><code>check</code><span> command. It traverses your views looking for Ruby Native calls like </span><code>native_navbar_tag</code><span> and </span><code>native_haptic_data</code><span>. Because Herb can process/analyze ERB, the </span><code>check</code><span> command can actually walk the AST and find bugs before you deploy. For example, having multiple &#8220;navbars&#8221; doesn&#8217;t make sense, but isn&#8217;t exactly a compilation error.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&#10140; bundle exec ruby_native check
app/views/books/index.html.erb
  17:  warning 2 elements carry `data-native-navbar`; only the first one is used.
&#8203;
Checked 18 templates: 0 errors, 1 warning.</code></pre></div><p><span>The most important check is that if you hook up your Ruby Native account, it can tell which native version you last deployed with. So if you try to add a Ruby Native feature that your users don&#8217;t yet have access to, it will spit out an error to update your native version.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&#10140; bundle exec ruby_native check --deployed
app/views/books/new.html.erb
  5:   error   `data-native-keyboard-toolbar` needs 0.15.0, your ios build is 0.14.2
&#8203;
Checked 18 templates: 1 error, 0 warnings.</code></pre></div><p><span>This takes a huge amount of stress off of Rails developers&#8217; shoulders. It means they can reliably and </span><em><span>safely</span></em><span> adopt Ruby Native features without worrying about breaking things, backwards compatibility, or accidentally using unsupported features. You can even hook it into your CI pipeline as a step to ensure it blocks a release if the check fails!</span></p><p><span>And the proof? Equipe is reporting less than 0.01% of users experiencing crashes across both platforms.</span></p><p><span>My long-term goal is to make mobile development easier and more accessible for Rails developers. And it&#8217;s hard to find a better example than this. A Rails founder is now running their mobile apps without stepping outside of Ruby. And doing it with confidence.</span></p><p><span>I&#8217;m proud of where Ruby Native has gone and I&#8217;m even more excited about what&#8217;s next. Being a finalist in the </span><a href="https://youtu.be/Rs5nMCQLcw4?si=vaC9UyDTM9S-j7Kr&amp;t=36"><span>Ruby Runway at RubyConf</span></a><span> was where it started to </span><em><span>feel</span></em><span> real. But now it&#8217;s like I have proof.</span></p><p><span>Enjoy Rails World later this month if you&#8217;re going. I wish I could be there!</span></p>]]></content:encoded></item><item><title><![CDATA[Goodbye… for now!]]></title><description><![CDATA[I'm going on parental leave and have paused paid subscriptions while I'm out and not writing new content.]]></description><link>https://newsletter.masilotti.com/p/goodbye-for-now</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/goodbye-for-now</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Fri, 28 Aug 2026 11:35:03 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cd399362-cc53-489d-8a9a-2b6de4dc679f_4284x4533.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Today is my last day of work until 2027. I&#8217;m going on parental leave for four months to welcome our third, a baby girl! She is due next week, which gives me </span><em><span>just enough</span></em><span> time to try and reset, recover from burnout, and attempt to catch up on all the little things before we have a newborn in the house again. &#129760;</span></p><p><span>One of the biggest trade-offs of working for myself is that I can take as much time off as possible for life events like this. The downside? It&#8217;s unpaid. Most of my income is from active engagement with clients: building apps, advisory work, or maintenance retainers. Which means, if I&#8217;m not working, I&#8217;m not making money!</span></p><p><span>Yes, there&#8217;s an exception with the &#8220;passive income&#8221; products I run. Most notably, Ruby Native. But those are annual plans, so I&#8217;m not seeing any </span><em><span>recurring</span></em><span> revenue until mid 2027. Unless, of course, someone signs up for an account! Speaking of, I just went through a huge audit on the codebase, hardening it for me not being around for a little while.</span></p><div class="twitter-embed" data-attrs="{&quot;url&quot;:&quot;https://x.com/joemasilotti/status/2092994185448280567?s=20&quot;,&quot;full_text&quot;:&quot;As I prepare to go on leave next week, I'm throwing the rest of my tokens into hardening Ruby Native.\n\nDocumentation is getting an audit to look for missing/inconsistent APIs, error messages are being fleshed out to include \&quot;what to do next\&quot; notes, and the underlying platform is&quot;,&quot;username&quot;:&quot;joemasilotti&quot;,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;profile_image_url&quot;:&quot;https://pbs.substack.com/profile_images/1654108046459813888/sFa_bOmY_normal.jpg&quot;,&quot;date&quot;:&quot;2026-08-27T15:14:34.000Z&quot;,&quot;photos&quot;:[],&quot;quoted_tweet&quot;:{},&quot;reply_count&quot;:2,&quot;retweet_count&quot;:0,&quot;like_count&quot;:10,&quot;impression_count&quot;:659,&quot;expanded_url&quot;:null,&quot;video_url&quot;:null,&quot;video_preview_media_key&quot;:null,&quot;belowTheFold&quot;:false}" data-component-name="Twitter2ToDOM"></div><p><span>So, what does that mean for the newsletter? In short, you probably won&#8217;t hear from me until 2027. So I&#8217;ve gone ahead and </span><strong><span>paused paid subscriptions for now</span></strong><span>. This means that if you are a paying subscriber you won&#8217;t be charged a renewal. I&#8217;ll make sure to let everyone know in advance before I turn them back on, so you&#8217;ll have time to decide if you want to continue your subscription or not.</span></p><p><span>What about outside of the newsletter?</span></p><ul><li><p><span>Ruby Native cloud builds and deploys run without me, that was the goal and it&#8217;s achieved!</span></p></li><li><p><span>Bridge Components buyers have everything but updates pause while I&#8217;m out.</span></p></li><li><p><span>The Discord stays up but I&#8217;ll be slow to respond to individual messages. I&#8217;m grateful for folks in the community stepping up and helping others.</span></p></li><li><p><span>General support waits until January.</span></p></li><li><p><span>Urgent requests (e.g. &#8220;production is down&#8221;) will be addressed as quickly as I can manage. Just be aware I might have a screaming baby on the call with me. &#128517;</span></p></li></ul><p><span>What can you expect from me, and this newsletter, when I return in January? First up will be an honest review of my 2026. This was the hardest year of my consulting business since I went independent in 2020. And I&#8217;ve had to adjust </span><em><span>a lot</span></em><span> in the past six months. I think I&#8217;ve found some things that might work in the future but I&#8217;d be lying if I said I have it all figured out.</span></p><p><span>The next four months are going to force me (in a good way!) to go on a lot of walks. I&#8217;m hoping to use that time to </span><em><span>really</span></em><span> figure out what I want next from my business. And which direction I want to take it. Most importantly, how can Masilotti.com go back to being a sustainable business, instead of always cramming an entire year of work into two months.</span></p><p><span>If you have thoughts, or ideas, or things to share&#8230; hit reply. Seriously! I would love to hear them. &#128075;</span></p>]]></content:encoded></item><item><title><![CDATA[What I wish Hotwire Native actually was]]></title><description><![CDATA[I wrote the book on Hotwire Native. Then I built what I actually wanted.]]></description><link>https://newsletter.masilotti.com/p/what-i-wish-hotwire-native-actually</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/what-i-wish-hotwire-native-actually</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Fri, 14 Aug 2026 12:31:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/689b41ac-f85b-4a42-a232-399ca7486ab2_2400x1350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>In 2021, Dale Zak published a post on dev.to called </span><a href="https://dev.to/dalezak/reusable-turbo-ios-project-configured-entirely-from-your-rails-app-3021"><span>Reusable Turbo iOS project configured entirely from your Rails app</span></a><span>. He set five goals for the project:</span></p><ol><li><p><span>Reusable base project that can be pointed to any Rails app</span></p></li><li><p><span>App styling, tabs, and navbar buttons driven from the server</span></p></li><li><p><span>Handle both authenticated and unauthenticated users</span></p></li><li><p><span>All logic and functionality contained in a single Swift file</span></p></li><li><p><span>No need for other developers to write any Swift code</span></p></li></ol><p><span>I was excited enough at the time to leave a comment: &#8220;Amazing work here! EVERYTHING is configurable via the Rails code without needing any Swift.&#8221;</span></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dMIU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dMIU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 424w, https://substackcdn.com/image/fetch/$s_!dMIU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 848w, https://substackcdn.com/image/fetch/$s_!dMIU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 1272w, https://substackcdn.com/image/fetch/$s_!dMIU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dMIU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png" width="1456" height="360" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:360,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:61071,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/211100939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dMIU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 424w, https://substackcdn.com/image/fetch/$s_!dMIU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 848w, https://substackcdn.com/image/fetch/$s_!dMIU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 1272w, https://substackcdn.com/image/fetch/$s_!dMIU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e254af7-0218-4d2c-b0b1-79d2613c5e67_1610x398.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p><span>First, take a look at the cute little avatar. Remember my Mii? &#128517;</span></p><p><span>But that was five years ago, and I haven&#8217;t stopped thinking about that list. Four of the five goals describe what I&#8217;ve been building. Honestly, almost exactly.</span></p><p><span>The one I&#8217;d change is number four. All the logic and functionality shouldn&#8217;t live in a single Swift file. It should live in your Rails app, and </span><strong><span>you should never have to look at the Swift at all</span></strong><span>.</span></p><h2><strong><span>I&#8217;m the Hotwire Native guy</span></strong></h2><p><span>I&#8217;ve been Hotwire Native&#8217;s biggest advocate since the day it was released. I wrote </span><a href="https://pragprog.com/titles/jmnative/hotwire-native-for-rails-developers"><span>Hotwire Native for Rails Developers</span></a><span> for The Pragmatic Bookshelf and I&#8217;ve built more than 30 apps with it.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!I77W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!I77W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 424w, https://substackcdn.com/image/fetch/$s_!I77W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 848w, https://substackcdn.com/image/fetch/$s_!I77W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!I77W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!I77W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2764753,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/211100939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!I77W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 424w, https://substackcdn.com/image/fetch/$s_!I77W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 848w, https://substackcdn.com/image/fetch/$s_!I77W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!I77W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfa77b4b-60eb-4c56-8ef4-9707353738fd_4032x3024.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><span>But I </span><em><span>still</span></em><span> think it asks too much of a Rails developer. Working through the book means writing Swift and Kotlin, keeping a Mac configured with Xcode, and debugging in the simulator. Every sharp edge that Rails spent two decades removing from web development comes right back.</span></p><p><span>Which is a fair price&#8230; if your goal is to learn mobile development. But for most Rails developers, </span><strong><span>the goal is to get in the App Store</span></strong><span>!</span></p><p><span>Those 30+ apps taught me a few things, but most importantly, they all come down to the same 95%:</span></p><ul><li><p><span>a native tab bar</span></p></li><li><p><span>a nav bar with a button or two</span></p></li><li><p><span>push notifications</span></p></li><li><p><span>a little native UI on top of the web views you already have</span></p></li></ul><p><span>The last 5% is real custom work. But in my opinion, </span><strong><span>that 95% shouldn&#8217;t require writing any native code at all</span></strong><span>.</span></p><h2><strong><span>Everything driven by your Rails app</span></strong></h2><p><strong><a href="https://rubynative.com/"><span>Ruby Native</span></a><span> is what I wish Hotwire Native actually was.</span></strong><span> No mobile experience, Mac, or Xcode required. </span><em><span>Everything is driven by your Rails server.</span></em></p><p><span>And by everything, I mean everything. New pages, new content, native buttons, push notifications, deep links, in-app purchases, and more. You deploy your Rails codebase like you always have and the native app picks up the changes immediately. The goal is for the iOS and Android apps to never need an update again.</span></p><p><span>Because native UI is rendered by your views, it follows your business logic. Here I&#8217;m adding a native button based on the user&#8217;s role in the system:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">&lt;%= native_navbar_tag &#8220;Orders&#8221; do |navbar| %&gt;
  &lt;% if current_user&amp;.admin? %&gt;
    &lt;%= navbar.button &#8220;Export&#8221;, icon: "share", href: export_orders_path %&gt;
  &lt;% elsif current_user %&gt;
    &lt;%= navbar.button &#8220;New order&#8221;, icon: "plus", href: new_order_path %&gt;
  &lt;% else %&gt;
    &lt;%= navbar.button &#8220;Sign in&#8221;, href: sign_in_path %&gt;
  &lt;% end %&gt;
&lt;% end %&gt;</code></pre></div><p><span>Admins get an Export button, signed-in users get New order, and everyone else gets Sign in. </span><strong><span>None of this business logic lives in the native codebase.</span></strong><span> It stays in Rails, instead of being split across iOS and Android, or React and React Native.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9rrX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9rrX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 424w, https://substackcdn.com/image/fetch/$s_!9rrX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 848w, https://substackcdn.com/image/fetch/$s_!9rrX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 1272w, https://substackcdn.com/image/fetch/$s_!9rrX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9rrX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png" width="600" height="456.62100456621005" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1000,&quot;width&quot;:1314,&quot;resizeWidth&quot;:600,&quot;bytes&quot;:475565,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/211100939?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9rrX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 424w, https://substackcdn.com/image/fetch/$s_!9rrX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 848w, https://substackcdn.com/image/fetch/$s_!9rrX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 1272w, https://substackcdn.com/image/fetch/$s_!9rrX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20211680-493d-4180-9d0e-24c2cf8f4569_1314x1000.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong><span>But&#8230; AI writes Swift now</span></strong></h2><p><span>So if AI writes native code, who cares what language the native layer is in?</span></p><p><span>Honestly&#8230; fair. Point Claude at my book and it will scaffold a Hotwire Native app that isn&#8217;t far from what I&#8217;d write by hand.</span></p><p><a href="https://newsletter.masilotti.com/p/i-went-off-the-deep-end-with-ai"><span>But writing the code was never the hard part.</span></a><span> It&#8217;s all the other stuff. AI can hand you a working Swift codebase, but it can&#8217;t get that codebase onto the phone in your pocket.</span></p><p><span>Then come signing certificates, App Store Connect, version numbers, screenshots&#8230; Don&#8217;t forget to do it all over again for Google Play! My book barely touches that part, because each app store could be its own 200+ pages.</span></p><p><strong><span>Ruby Native handles all of this for you</span></strong><span>. Connect your App Store account with one API key and Ruby Native signs, builds, screenshots, and distributes your app for you. Everything that&#8217;s a pain in the butt for a Rails developer is just, well, handled.</span></p><h2><strong><span>Keep writing Ruby</span></strong></h2><p><span>I don&#8217;t want a Rails developer to have to learn Swift to launch in the App Store, or Kotlin to launch on Google Play, or to maintain three codebases when one has been paying the bills all along. I want them to keep writing Ruby, push to production like they always have, and watch their app update itself.</span></p><p><span>That&#8217;s what I wish Hotwire Native was. So I built it: </span><a href="https://rubynative.com/"><span>rubynative.com</span></a></p>]]></content:encoded></item><item><title><![CDATA[Ruby Native, now on Android!]]></title><description><![CDATA[Android out of beta, barcode scanning, and a smarter, more customizable navigation bar.]]></description><link>https://newsletter.masilotti.com/p/ruby-native-now-on-android</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/ruby-native-now-on-android</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 05 Aug 2026 15:37:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!W7Fd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Hey there, I&#8217;m excited to announce the biggest release of </span><a href="https://rubynative.com/"><span>Ruby Native</span></a><span> since launch: </span><strong><span>Android support</span></strong><span>!</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!W7Fd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!W7Fd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 424w, https://substackcdn.com/image/fetch/$s_!W7Fd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 848w, https://substackcdn.com/image/fetch/$s_!W7Fd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 1272w, https://substackcdn.com/image/fetch/$s_!W7Fd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!W7Fd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png" width="506" height="558.0882352941177" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1350,&quot;width&quot;:1224,&quot;resizeWidth&quot;:506,&quot;bytes&quot;:872510,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/209853468?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!W7Fd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 424w, https://substackcdn.com/image/fetch/$s_!W7Fd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 848w, https://substackcdn.com/image/fetch/$s_!W7Fd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 1272w, https://substackcdn.com/image/fetch/$s_!W7Fd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4afc630-6338-45cd-9a12-5ec3afcb00e3_1224x1350.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><a href="https://github.com/ruby-native/gem/releases/tag/v0.12.0"><span>v0.12</span></a><span> officially brings Android out of beta. If you already have a working iOS app with Ruby Native, then you&#8217;re ready for Android. Few to no changes are needed in your Rails codebase to support the new platform.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NRXd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NRXd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 424w, https://substackcdn.com/image/fetch/$s_!NRXd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 848w, https://substackcdn.com/image/fetch/$s_!NRXd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 1272w, https://substackcdn.com/image/fetch/$s_!NRXd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NRXd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png" width="1456" height="797" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:797,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:114601,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/209853468?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NRXd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 424w, https://substackcdn.com/image/fetch/$s_!NRXd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 848w, https://substackcdn.com/image/fetch/$s_!NRXd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 1272w, https://substackcdn.com/image/fetch/$s_!NRXd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb512919-d91e-432d-ae5c-ebb7ba77560c_1546x846.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><span>With the exception of in-app purchases, Ruby Native on Android can now do everything the iOS version can do. This includes cross-tab links, back-button history, share buttons, form handling, and push notifications.</span></p><p><span>Outside of Android support, a bunch of other improvements have launched in the past few weeks.</span></p><h2><strong><span>Securely sign out users</span></strong></h2><p><span>Add the following line to your layout and Ruby Native will securely reset the app when your users sign out:</span></p><pre><code><span>&lt;%= native_identity_tag current_user&amp;.id %&gt;</span></code></pre><p><span>Check out the full </span><a href="https://rubynative.com/docs/authentication"><span>authentication documentation</span></a><span> for how this behaves in different scenarios and how to configure it if you have accounts or teams (and not just users).</span></p><h2><strong><span>Scan barcodes</span></strong></h2><p><span>Another big feature available on iOS and Android is barcode scanning. Add some markup to your form to trigger a native, platform-specific barcode scanner flow:</span></p><pre><code><span>&lt;%= form_with url: lookups_path do |form| %&gt;</span>
<span>  &lt;%= form.text_field :isbn, id: "isbn" %&gt;</span>
<span>  &lt;%= native_scan_button_tag "Scan", target: "#isbn" %&gt;</span>
<span>&lt;% end %&gt;</span></code></pre><p><span>Add </span><code>submit: true</code><span> to submit the field&#8217;s form automatically after a successful scan, for example when scanning a book&#8217;s barcode and running a search against it on your server.</span></p><h2><strong><span>Customize the navigation bar</span></strong></h2><p><span>You can now further customize the navigation with title menus, segments, and branding.</span></p><p><span>Turn the native title into a </span><a href="https://rubynative.com/docs/navbar#title-menu"><span>dropdown menu</span></a><span> with </span><code>position: :title</code><span>. Tapping the title opens the menu, and the selected item shows a checkmark.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4kLT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4kLT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 424w, https://substackcdn.com/image/fetch/$s_!4kLT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 848w, https://substackcdn.com/image/fetch/$s_!4kLT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 1272w, https://substackcdn.com/image/fetch/$s_!4kLT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4kLT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png" width="1533" height="630" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:630,&quot;width&quot;:1533,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:201734,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/209853468?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5785fde-5c37-4818-9f26-4c16716329d1_1540x802.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4kLT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 424w, https://substackcdn.com/image/fetch/$s_!4kLT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 848w, https://substackcdn.com/image/fetch/$s_!4kLT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 1272w, https://substackcdn.com/image/fetch/$s_!4kLT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f92b2d5-4522-43ef-853b-ffb44cbe515a_1533x630.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code><span>&lt;%= native_navbar_tag "Inbox" do |navbar| %&gt;</span>
<span>  &lt;%= navbar.button position: :title do |menu| %&gt;</span>
<span>    &lt;%= menu.item "Inbox",</span>
<span>        href: inbox_path,</span>
<span>        icon: "tray",</span>
<span>        selected: true %&gt;</span>
<span>&#8203;</span>
<span>    &lt;%= menu.item "Archive",</span>
<span>        href: archive_path,</span>
<span>        icon: "archivebox" %&gt;</span>
<span>  &lt;% end %&gt;</span>
<span>&lt;% end %&gt;</span></code></pre><p><span>Add </span><a href="https://rubynative.com/docs/navbar#segments"><span>native segments</span></a><span> with </span><code>navbar.segment</code><span> to quickly switch between contexts without leaving the current screen.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DX3F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DX3F!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 424w, https://substackcdn.com/image/fetch/$s_!DX3F!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 848w, https://substackcdn.com/image/fetch/$s_!DX3F!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 1272w, https://substackcdn.com/image/fetch/$s_!DX3F!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DX3F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png" width="1546" height="970" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:970,&quot;width&quot;:1546,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:100180,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/209853468?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc0a9a6b-30fc-438a-95e1-1004c1f7b256_1546x1146.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DX3F!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 424w, https://substackcdn.com/image/fetch/$s_!DX3F!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 848w, https://substackcdn.com/image/fetch/$s_!DX3F!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 1272w, https://substackcdn.com/image/fetch/$s_!DX3F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba72bf83-873c-4316-98ba-eb9e1b844165_1546x970.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code><span>&lt;%= native_navbar_tag do |navbar| %&gt;</span>
<span>  &lt;%= navbar.segment "Today", href: today_path, selected: true %&gt;</span>
<span>  &lt;%= navbar.segment "Upcoming", href: upcoming_path %&gt;</span>
<span>  &lt;%= navbar.segment "Anytime", href: anytime_path %&gt;</span>
<span>&lt;% end %&gt;</span></code></pre><p><span>Or completely customize the </span><a href="https://rubynative.com/docs/appearance#navbar-branding"><span>branding of the navigation bar</span></a><span> with a background color and logo.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">appearance:
  navbar:
    logo: "&lt;%= image_url(&#8217;logo.png&#8217;) %&gt;"
    background_color: "#3B3F54"
    foreground_color: "#FFFFFF"
    status_bar: light</code></pre></div><h2><strong><span>What&#8217;s next?</span></strong></h2><p><span>Up next is building in-app purchase support for Android. From there, I&#8217;m planning on doing a bit of experimentation. Without giving away too much, </span><em><span>what if Hotwire Native worked with Inertia?</span></em></p><p><span>Hit reply if you want a sneak peek! Until then, you can start building your own mobile apps with </span><a href="https://rubynative.com/"><span>Ruby Native</span></a><span>.</span></p>]]></content:encoded></item><item><title><![CDATA[Hotwire Native v1.3]]></title><description><![CDATA[Lazy loading tabs, UISearchTab, Large Titles, custom error views, and a whole lotta bug fixes.]]></description><link>https://newsletter.masilotti.com/p/hotwire-native-v13</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/hotwire-native-v13</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 08 Jul 2026 13:03:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Cpqp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Hey everyone, Hotwire Native v1.3 just dropped with a </span><em><span>huge</span></em><span> release notes entry.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Cpqp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Cpqp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 424w, https://substackcdn.com/image/fetch/$s_!Cpqp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 848w, https://substackcdn.com/image/fetch/$s_!Cpqp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 1272w, https://substackcdn.com/image/fetch/$s_!Cpqp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Cpqp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png" width="1456" height="1300" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1300,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:547672,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/205966156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Cpqp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 424w, https://substackcdn.com/image/fetch/$s_!Cpqp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 848w, https://substackcdn.com/image/fetch/$s_!Cpqp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 1272w, https://substackcdn.com/image/fetch/$s_!Cpqp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d8a301-0bc4-4f6a-a9d6-6c2401a772b3_1816x1622.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><span>And that&#8217;s just for iOS! Here&#8217;s the stuff that matters, including the breaking changes you&#8217;ll need to accommodate in your app to upgrade today.</span></p><h2><span>iOS</span></h2><p><span>iOS saw the majority of the work this release, mostly focused on adding customization options to the included native pieces and reaching API parity with Android.</span></p><h3><span>Lazy loading native tabs</span></h3><p><span>Native tabs can now be lazy-loaded, meaning launching the app won&#8217;t hit your server with 5+ network requests. When enabled, each tab&#8217;s content loads the first time the user opens it. Note that this is a breaking change, as it was moved from </span><code>Hotwire.config.lazyLoadTabs</code><span>.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">HotwireTabBarController(lazyLoadTabs: true)</code></pre></div><h3><span>Search tabs</span></h3><p><span>For users running iOS 26, you can use the new </span><code>UISearchTab</code><span> to show a floating tab off to the right. Tapping this opens the tab&#8217;s content as usual. The only difference is the special placement.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">HotwireTab(
    title: &#8220;Search&#8221;,
    image: UIImage(systemName: &#8220;magnifyingglass&#8221;),
    url: URL(string: &#8220;...&#8221;)!,
    isSearchTab: true
)</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a489!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a489!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 424w, https://substackcdn.com/image/fetch/$s_!a489!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 848w, https://substackcdn.com/image/fetch/$s_!a489!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 1272w, https://substackcdn.com/image/fetch/$s_!a489!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a489!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png" width="1408" height="466" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5097d2f6-2585-438c-956f-230f6a026101_1408x466.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:466,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:100506,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/205966156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!a489!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 424w, https://substackcdn.com/image/fetch/$s_!a489!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 848w, https://substackcdn.com/image/fetch/$s_!a489!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 1272w, https://substackcdn.com/image/fetch/$s_!a489!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5097d2f6-2585-438c-956f-230f6a026101_1408x466.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><span>Large titles</span></h3><p><span>A long-awaited fix for &#8220;large titles&#8221; was also merged, enabling proper behavior when customizing the navigation controller used by Hotwire Native.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class CustomNavigationController: HotwireNavigationController {
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationBar.prefersLargeTitles = true
    }
}
&#8203;
Hotwire.config.defaultNavigationController = { CustomNavigationController() }</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rzWE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rzWE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 424w, https://substackcdn.com/image/fetch/$s_!rzWE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 848w, https://substackcdn.com/image/fetch/$s_!rzWE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 1272w, https://substackcdn.com/image/fetch/$s_!rzWE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rzWE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png" width="1408" height="616" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:616,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:91185,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/205966156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rzWE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 424w, https://substackcdn.com/image/fetch/$s_!rzWE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 848w, https://substackcdn.com/image/fetch/$s_!rzWE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 1272w, https://substackcdn.com/image/fetch/$s_!rzWE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08106bb2-f737-48a1-b509-fe8808a823aa_1408x616.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><span>Custom error views</span></h3><p><span>Native error views can now be fully customized, even with SwiftUI.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Hotwire.config.makeCustomErrorView = { error, handler in
    AnyView(MyErrorView(error: error, onRetry: handler))
}
&#8203;
struct MyErrorView: View {
    let error: Error
    let onRetry: ErrorPresenter.Handler?
&#8203;
    var body: some View {
        // ...
    }
}</code></pre></div><p><span>Also, </span><code>TurboError</code><span> is gone, replaced by a structured </span><code>HotwireNativeError</code><span> (</span><code>HTTPError</code><span> / </span><code>LoadError</code><span> / </span><code>WebError</code><span>), and a retry handler is now always supplied. So if you referenced those directly in your codebase you&#8217;ll need to update them.</span></p><p><span>There were also a bunch of bug fixes that were finally merged, including commits from six new contributors! Check out the </span><a href="https://github.com/hotwired/hotwire-native-ios/releases/tag/1.3.0"><span>full release notes</span></a><span> for all the details.</span></p><h2><span>Android</span></h2><p><span>Android&#8217;s big improvement was the addition of lazy-loaded tabs, to match the behavior on iOS. There were also a lot of under-the-hood bug fixes and cleanup, including resolving some weird crashes that one of my clients was experiencing.</span></p><p><span>The only breaking changes for Android are around </span><code>HotwireDestination.customRouteDecision()</code><span> and some logging cleanup.</span></p><p><span> </span><code>Hotwire.config.debugLoggingEnabled</code><span> is removed, replaced by a pluggable </span><code>Hotwire.config.logger</code><span>. Set </span><code>logger.logLevel = HotwireLogLevel.DEBUG</code><span> for debug output. See the </span><a href="https://github.com/hotwired/hotwire-native-android/releases/tag/1.3.0"><span>full release notes</span></a><span> for more.</span></p><h2><span>Demo app</span></h2><p><span>The </span><a href="https://hotwire-native-demo.dev/"><span>Hotwire Native demo app</span></a><span> was improved to work with the new changes across both platforms and now acts as a test bed for reproducing bugs. This should make it a bit easier for me and the other maintainers of Hotwire Native to identify and fix issues faster.</span></p><p><span>If you upgrade to v1.3 and run into any issues please let me know. I&#8217;m sure v1.3.1 isn&#8217;t far behind!</span></p><div><hr></div><p><span>P.S. Who&#8217;s heading to RubyConf next week? I&#8217;m hosting a </span><a href="https://rubyconf.org/schedule/#wednesday"><span>Ruby Native workshop on Wednesday</span></a><span> and I&#8217;d love to meet you IRL! Hit reply and let me know so we can grab a beverage together.</span></p>]]></content:encoded></item><item><title><![CDATA[The road expands before me]]></title><description><![CDATA[How my product's North Star makes it easier to know what to build, and when.]]></description><link>https://newsletter.masilotti.com/p/the-road-expands-before-me</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/the-road-expands-before-me</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Fri, 26 Jun 2026 16:22:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/61dbb014-3999-4f15-9f85-ba0470c76152_4629x2603.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Credit to Aaron Francis for this one. He said it on a recent </span><a href="https://mostlytechnical.com/"><span>podcast episode</span></a><span> and it really stuck with me. He was talking about his business that </span><strong><span>no matter how much he gets done there&#8217;s always more to do</span></strong><span>.</span></p><p><span>I&#8217;m feeling the same with Ruby Native right now.</span></p><div class="callout-block" data-callout="true"><p><em><a href="https://rubynative.com/"><span>Ruby Native</span></a> is my productized serve that gets your Rails business into the app stores without a single line of native code.</em></p></div><p><span>Aaron&#8217;s point is that the list of things to build never ends. But the part he didn&#8217;t get into is that </span><strong><span>reaching the end was never the goal</span></strong><span>. Knowing what to skip is.</span></p><p><span>Last week I signed a Concierge client to take their Rails website to mobile. I&#8217;m in charge of </span><em><span>everything</span></em><span>, from setting up Ruby Native to tweaking their design to work better on mobile, to adding native components when it makes sense, to submitting to the App Store and dealing with App Store review. Essentially, </span><strong><span>they are paying me to do 100% of the &#8220;get us in the App Store&#8221; work</span></strong><span>.</span></p><p><span>This isn&#8217;t too different than what I&#8217;ve been doing for 6+ years for Hotwire Native. With those clients, they&#8217;d have the exact same need: get in the App Store. But back then, every single client was a unique, bespoke build. If they wanted specific native components, then those would be built on the fly, just for them.</span></p><p><span>Sometimes I could abstract or borrow from </span><a href="https://bridgecomponents.dev/"><span>my bridge component library</span></a><span>, but oftentimes they were </span><em><span>just</span></em><span> different enough that I had to build something from scratch. But as always, </span><a href="https://newsletter.masilotti.com/p/i-used-to-own-a-tiny-niche"><span>the code was never the hard part</span></a><span>. It&#8217;s always about the other stuff: what to build, where to integrate it, and how to get it live (App Store approval is </span><em><span>still</span></em><span> no joke!).</span></p><p><span>Seeing this client go through the process, and me slowly tweaking their app to look and feel better (which I can&#8217;t wait to share as a case study when they go live!), has expanded the Ruby Native roadmap even more. From just three days of working with them I&#8217;ve noticed some common patterns that would make their app feel </span><em><span>so much</span></em><span> more native. And could easily be abstracted to work with other customer apps.</span></p><p><span>But every one of those patterns I abstract is one more thing to build. As I work with more folks, and as more developers get their hands on the Ruby Native apps, the feature requests and obvious niceties are growing. My </span><a href="https://github.com/orgs/ruby-native/projects/1"><span>public roadmap</span></a><span> hasn&#8217;t shrunk since I announced it. &#128517;</span></p><p><span>There&#8217;s a point where Ruby Native can do almost the entire native stack, entirely driven by HTML. The decision stops being &#8220;is this custom enough to go Hotwire Native?&#8221; and becomes, </span><strong><span>just go Ruby Native, it will (almost) always be enough</span></strong><span>.</span></p><p><span>And maybe that&#8217;s OK! Sure, it gives me a bit of anxiety to never close out a todo list. But it also means there&#8217;s always something new and exciting to work on. </span><strong><span>And with AI? It makes the </span></strong><em><strong><span>what</span></strong></em><strong><span> even more important.</span></strong><span> I could technically build every little thing a (potential) customer asks for. But at what point does it start to dilute the primary purpose of Ruby Native? At what point does it become such a complicated tool that it&#8217;s hard to add new code? Or worse, that it becomes </span><em><span>confusing</span></em><span> to the developer trying to build with it?</span></p><p><span>That&#8217;s a tough call. And a lot of folks online will say it&#8217;s where someone&#8217;s </span><em><span>taste</span></em><span> comes into play.</span></p><p><span>I like that word, but I also think it&#8217;s a bit loaded. I prefer to think of it as a solid product </span><em><span>vision</span></em><span>. </span><strong><span>Knowing exactly what your product or service does is almost as important as actually doing it</span></strong><span>. For me, that means having a </span><em><span>north star</span></em><span> that I build everything around: making it easier for Rails developers to get in the app stores.</span></p><p><span>Following this for every new feature almost makes the decisions easier. Someone recently asked if Ruby Native could be a package, like Hotwire Native, that you drop into your own codebase. It&#8217;s a fair question, but it runs against the whole point of Ruby Native: I never want a Rails developer to have to touch native code.</span></p><p><span>But a new API just dropped on iOS that makes it easier to do something? Obvious yes! Wire it up to some HTML-powered API and </span><em><span>boom</span></em><span>, every Ruby Native developer gets it for free the next time they deploy.</span></p><p><span>Perhaps the expanding road isn&#8217;t the problem I made it out to be. The north star doesn&#8217;t make the list shorter, </span><strong><span>it tells me which items to ignore</span></strong><span>. And </span><em><span>that</span></em><span>, as a solo-run business, is the most important thing.</span></p><p><span>But there&#8217;s one question the north star can&#8217;t answer. Building new features doesn&#8217;t bring in customers. I know this and every developer &#8220;knows&#8221; this. The north star tells me </span><em><span>what</span></em><span> to build, but it can&#8217;t tell me </span><em><span>when</span></em><span> to build it. Or when to write a case study, or when to do cold outreach, or when to start paid advertising, or... well you get the idea: sales and marketing!</span></p><p><span>So, if the road expands before me, is that good? Is that bad?</span></p><p><span>If you know, please tell me. Because I sure don&#8217;t know yet!</span></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/p/the-road-expands-before-me/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.masilotti.com/p/the-road-expands-before-me/comments"><span>Leave a comment</span></a></p>]]></content:encoded></item><item><title><![CDATA[I stopped tracking my time. Now I can't focus.]]></title><description><![CDATA[I'm getting more done than ever, and I've never felt more scattered.]]></description><link>https://newsletter.masilotti.com/p/i-stopped-tracking-my-time-now-i</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/i-stopped-tracking-my-time-now-i</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 11 Jun 2026 19:53:38 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4ea448c5-97c0-4030-9454-62d5c7f16b94_4288x2848.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the most freeing things I&#8217;ve ever done in my business is to stop tracking time.</p><p>I used to religiously track where I spent my time when on my computer. Client work in one bucket, personal branding in another, and side projects/adventures in a third. Inside of those, I would then categorize <em>which</em> engagement/project I was working on.</p><p>At the end of the year, it was awesome! I had this huge breakdown of where all of my time went. I could even cross-reference it against what each thing made money-wise and figure out my effective billable rate. Which sometimes was super empowering. But for side projects? Super depressing. :P</p><p>I&#8217;ve launched more than a handful of &#8220;things&#8221; that made no money at all. And spent a <em>ton</em> of time working on them.</p><p>As time went on, I found myself taking a few seconds to pick which project to bill time to. &#8220;Ok, let&#8217;s go build this thing!&#8221; &#8220;Oh, let me track time first.&#8221; &#8220;Hmm, is this personal branding? Marketing? A side project that I haven&#8217;t solidified yet?&#8221;</p><p>That additional context switch (on top of the actual one) could often kill the idea I had in my head. Before I even started working, I all of a sudden had an admin task to take care of.</p><p>And sure, I could do it after the fact. So I started just clicking &#8220;start&#8221; without assigning a project in my time tracker. Which worked pretty well! But then I would forget entirely. Or even worse, switch tasks and now had to make <em>two</em> decisions (old work and new work).</p><p>I eventually realized that it was costing me more time and, more importantly, <em>mental energy</em>, to keep this up than the payoff was worth.</p><p><strong><mark data-color="#d9ead3" style="background-color: rgb(217, 234, 211); color: rgb(0, 0, 0);">So in 2026, I completely stopped tracking my time. And it has been the most freeing thing I&#8217;ve ever done for my business.</mark></strong></p><p>When an idea sparks, I just... do it. When I want to context switch, well, I context switch!</p><p>And with AI-assisted development? It means I can bounce around like all freaking day working on whatever I want without any repercussions!</p><p>Except&#8230; not exactly.</p><p>Now I&#8217;m bouncing around all the freaking time. Why work on one thing when I could work <strong>on ten things</strong>?! Why limit my creativity to a single project that doesn&#8217;t make money when I could be building <strong>twenty things that don&#8217;t make money</strong>?!</p><p>And so on and so on.</p><p>Turns out, the friction I felt around picking <strong>one thing</strong> may have actually been beneficial. Perhaps it was actually <em>helping me stay focused</em>. Even if it cost just a bit of extra time before I sat down and worked.</p><p>I&#8217;m not sure if AI <em>caused</em> this or just threw lighter fluid on an existing fire. But whatever it is&#8230; damn is my brain fragmented.</p><p>I feel like I&#8217;m getting so much done. But also so mentally exhausted at the end of the day.</p><p>I also really want to work&#8230; <em>more</em>? It&#8217;s quite a feeling. And maybe all this is just my ADHD getting dopamine hit after dopamine hit when I&#8217;m working with Claude.</p><p>Or maybe this is just the new way of working?</p>]]></content:encoded></item><item><title><![CDATA[I deleted WebSockets and haven’t looked back]]></title><description><![CDATA[Why I swapped Turbo Stream broadcasts for polling after a paying customer got stuck with an infinite spinner.]]></description><link>https://newsletter.masilotti.com/p/i-deleted-websockets-and-havent-looked</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/i-deleted-websockets-and-havent-looked</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 04 Jun 2026 16:15:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b0394fe9-124b-489c-8b41-ecb0f936b184_1200x630.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"><em>I help Rails developers ship iOS and Android apps. I&#8217;ve shipped 25+ apps, wrote the book on Hotwire Native, and share what I&#8217;m learning about running a business without burning out.</em></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Hey friends,</p><p>Ian Landsman posted something recently. And at first, I totally dismissed it. &#8220;Old man yells at cloud!&#8221; I was saying in my head.</p><div class="twitter-embed" data-attrs="{&quot;url&quot;:&quot;https://x.com/IanLandsman/status/2060883601575539037&quot;,&quot;full_text&quot;:&quot;Ripped every single websocket call out of <span class=\&quot;tweet-fake-link\&quot;>@outrocrew</span> and moved to polling the server and everything works much better &#128516; sometimes the old ways are the best ways.&quot;,&quot;username&quot;:&quot;IanLandsman&quot;,&quot;name&quot;:&quot;Ian Landsman&quot;,&quot;profile_image_url&quot;:&quot;https://pbs.substack.com/profile_images/2020360383027113984/gVPOhji9_normal.jpg&quot;,&quot;date&quot;:&quot;2026-05-31T00:38:34.000Z&quot;,&quot;photos&quot;:[],&quot;quoted_tweet&quot;:{},&quot;reply_count&quot;:7,&quot;retweet_count&quot;:2,&quot;like_count&quot;:56,&quot;impression_count&quot;:12142,&quot;expanded_url&quot;:null,&quot;video_url&quot;:null,&quot;video_preview_media_key&quot;:null,&quot;belowTheFold&quot;:false}" data-component-name="Twitter2ToDOM"></div><p>He ripped every WebSocket call out of his app, moved to polling the server, and said everything just works better. &#8220;Sometimes the old ways are the best ways.&#8221;</p><p>But for some reason&#8230; it stuck with me. Probably because I&#8217;d been doing the exact opposite all over my own dashboard, and had been bitten by it twice.</p><p>A customer paid for a subscription and got dropped on a dead-end spinner that never resolved. The payment went through, Stripe was happy, but the page just never heard about it. A few days before that, someone breezing through onboarding missed a single broadcast and got stuck on a step that never advanced.</p><p>So I (begrudgingly) followed Ian&#8217;s lead and started deleting. Here&#8217;s the whole story, plus something I picked up only after the code was gone.</p><h2><strong>I was broadcasting everything with Turbo Streams</strong></h2><p>The dashboard for <a href="https://rubynative.com/">Ruby Native</a> does a lot of waiting on background jobs. It validates your App Store Connect credentials, checks whether your beta testers got added, watches a build move through GitHub Actions, confirms an in-app purchase webhook, and validates a Stripe subscription.</p><p>Every one of those started the same way: a background job did its work then broadcast the result to the page over Turbo Streams. Add an <code>after_commit</code> on the model, a <code>turbo_stream_from</code> in the view, and the result appears without a refresh. It&#8217;s the default UX in Hotwire, and it feels great when you wire it up.</p><p>The problem is what happens when the broadcast doesn&#8217;t land.</p><h2><strong>Broadcasts are fire and forget</strong></h2><p>A Turbo Stream broadcast is a single message pushed down an open socket. If nobody&#8217;s listening at that exact moment, it&#8217;s gone. There&#8217;s no retry or catch-up.</p><p>There are two easy ways to miss one. The first is a race: the job finishes and broadcasts <em>before</em> the browser has finished subscribing to the channel, so the message goes out to an empty room. The second is simpler, and way more likely: the connection blips, the message is lost, and nothing ever asks for it again.</p><p>Either way the page sits there. And on a few of these screens the spinner <em>was</em> the default state in the database, so when the result never arrived, nothing ever replaced it. The page wasn&#8217;t mid-update, it was stuck, forever, behind a sad little &#8220;try refreshing&#8221; link I&#8217;d bolted on as a patch.</p><p>That patch is what bothered me. If the answer to &#8220;my real-time UI didn&#8217;t update&#8221; is &#8220;ask the user to refresh,&#8221; then the real-time part isn&#8217;t carrying its weight, no?</p><h2><strong>What if the dashboard just&#8230; </strong><em><strong>asked</strong></em><strong>?</strong></h2><p>So I asked myself a question I probably should have asked at the start. <strong>Is pushing just overkill for a dashboard?</strong></p><p>A dashboard isn&#8217;t a chat app. The updates I care about land seconds (sometimes minutes!) apart, not milliseconds. Nobody needs sub-second delivery to find out their credentials validated. What they need is for the page to be <em>correct</em>, and to recover on its own when something goes wrong.</p><p>The database already knew the truth the entire time. Broadcasting was just trying to mirror that truth into an open socket and occasionally missing. So I flipped it around. Instead of the job pushing the result out, the page re-asks the server &#8220;what&#8217;s true now?&#8221; every few seconds, and stops asking as soon as it has its answer.</p><h2><strong>Move the transient state into a row</strong></h2><p>The first piece is giving each async step somewhere durable to record what happened. I added one small model, <code>OnboardingCheck</code>, with a row per app per step. It holds only the transient state of the last attempt: whether a job is in flight and the error if one came back. The authoritative &#8220;this step passed&#8221; signal stays where it always lived, on the parent record, <code>App</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">class OnboardingCheck &lt; ApplicationRecord
  belongs_to :app
&#8203;
  # How long a stamped checking_at counts as &#8220;a job is in flight&#8221; before
  # the page assumes the worker died and re-runs the check on the next poll.
  CHECK_TTL = 2.minutes
&#8203;
  def checking?
    checking_at.present? &amp;&amp; checking_at.after?(CHECK_TTL.ago)
  end
&#8203;
  def start!
    update!(checking_at: Time.current, error: nil, reason: nil)
  end
&#8203;
  def finish!(error: nil, reason: nil)
    update!(checking_at: nil, error:, reason:)
  end
end</code></pre></div><p>The job calls <code>start!</code> when it kicks off and <code>finish!</code> when it&#8217;s done. Notice what that buys you: the error is now persisted. A failure survives a refresh, where before it lived for one broadcast and then vanished.</p><p><code>CHECK_TTL</code> is the escape hatch. Since these checks <em>should</em> finish in a second or two, a <code>checking_at</code> older than two minutes means the worker died before reporting back, so the next poll re-runs the check instead of leaving the user stuck on a spinner.</p><h2><strong>A frame that reloads itself until it&#8217;s done</strong></h2><p>The second piece is a Turbo Frame that polls. While a job is in flight, the frame renders a small Stimulus controller that reloads the frame on a timer. When the result lands, the re-render simply leaves that controller out, and the polling stops on its own.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&lt;%= turbo_frame_tag &#8220;subscription_status&#8221;, src: confirm_checkout_path(@app) do %&gt;
  &lt;% if @app.active_subscription? %&gt;
    &lt;%= render &#8220;checkouts/subscription_confirmed&#8221;, app: @app %&gt;
  &lt;% else %&gt;
    &lt;p&gt;Waiting for payment confirmation...&lt;/p&gt;
    &lt;div data-controller=&#8221;poll&#8221; data-poll-interval-value=&#8221;3000&#8221;&gt;&lt;/div&gt;
  &lt;% end %&gt;
&lt;% end %&gt;</code></pre></div><p>The controller is about as small as it gets.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// app/javascript/controllers/poll_controller.js
import { Controller } from &#8220;@hotwired/stimulus&#8221;
&#8203;
export default class extends Controller {
  static values = { interval: { type: Number, default: 5000 } }
&#8203;
  connect() {
    this.timer = setInterval(() =&gt; {
      this.element.closest(&#8221;turbo-frame&#8221;)?.reload()
    }, this.intervalValue)
  }
&#8203;
  disconnect() {
    clearInterval(this.timer)
  }
}</code></pre></div><p>This is the part I find genuinely tidy. The controller only exists in the DOM while there&#8217;s work to wait on. The moment the success partial renders instead, the controller&#8217;s element is gone, Stimulus fires <code>disconnect()</code>, <code>clearInterval</code> runs, and the polling ends. There&#8217;s nothing to remember to tear down.</p><p>I wrapped that pattern in a <code>poll_frame</code> helper so every surface shares it, but the mechanism is exactly the two snippets above.</p><h2><strong>Three things that bit me</strong></h2><p>If you try this, here are the gotchas I hit so you can (hopefully) avoid them.</p><p><strong>A self-referencing </strong><code>src</code><strong> makes Turbo complain.</strong> A frame whose <code>src</code> points at the same URL it&#8217;s already showing gets rejected. On the polling re-render I leave <code>src</code> off and let the frame keep the one it was born with. The Stimulus <code>reload()</code> still re-fetches that original <code>src</code> just fine.</p><p><strong>Polling isn&#8217;t free when a poll hits an external API.</strong> My build feed re-renders each app&#8217;s Google Play status, and that&#8217;s a real API call, not a database read. Left alone, polling would quietly hammer Google&#8217;s rate limit. To fix this, I cache that lookup for 30 seconds, and force a fresh read only right after a rollout so the result still shows up immediately.</p><p><strong>Links inside a polling frame need an escape hatch.</strong> By default a link inside a Turbo Frame tries to navigate <em>that frame</em>, and any link to a page without a matching frame id throws Turbo&#8217;s &#8220;Content missing.&#8221; Set <code>target: "_top"</code> on the frame so links and forms do a normal full-page visit. The frame&#8217;s own reloading is unaffected.</p><p><strong>And one decision worth making on purpose: what should a 500 do?</strong> I made errors during the flow bubble up and stop the polling, so the user sees that something broke and knows to refresh. A failure you can see beats a failure that hides behind a spinner.</p><h2><strong>The lesson wasn&#8217;t really &#8220;polling&#8221;</strong></h2><p>The reliability didn&#8217;t come from polling. <strong>It came from rendering off the database every single time.</strong></p><p>A poll tick and a broadcast are both just &#8220;go show the current state.&#8221; The difference is that polling has no choice but to recompute from the source of truth, while a broadcast lets you push the rendered change and skip that step. That skip is exactly where the spinner bug lived!</p><p>Which means I didn&#8217;t strictly have to drop the socket. If each broadcast had only said &#8220;something changed, re-fetch&#8221; instead of carrying the result itself, the page would recompute from the database and heal the same way. That&#8217;s more or less what Turbo 8&#8217;s <code>broadcast_refresh</code> and morphing do: the broadcast says &#8220;re-render,&#8221; and the morph reconciles against fresh server HTML. You get push&#8217;s snappiness with polling&#8217;s self-healing.</p><p>I still went with polling, because a dashboard doesn&#8217;t need the snappiness and polling makes that recompute-from-truth discipline automatic instead of optional. But that&#8217;s the rule I&#8217;m keeping, and it&#8217;s the sharper version of Ian&#8217;s point: <strong>pick your transport for how fast and how wide you need to push, and render from durable state no matter what. Push a hint, not a delta.</strong></p><h2><strong>I didn&#8217;t delete Action Cable</strong></h2><p>To be clear, I didn&#8217;t rip WebSockets out of the building. <code>solid_cable</code> is still mounted, and a couple of genuinely live surfaces still push. Polling isn&#8217;t <em>automatically</em> the better tool.</p><p>It&#8217;s the better tool <em>here</em>, on a dashboard where the database is the source of truth and &#8220;a few seconds late&#8221; is completely fine. If I were building live chat or a collaborative editor, I&#8217;d still reach for push without thinking twice.</p><p><strong>The mistake wasn&#8217;t using broadcasts, it was using them as the default for everything.</strong> Like the checkout page where a single lost message left a paying customer staring at a spinner.</p><p>Thanks for the nudge, Ian. Turns out the old ways really are the best ways.</p><p>Where have you reached for a broadcast when a poll, or a plain re-fetch, would have done the job? Hit reply and let me know!</p>]]></content:encoded></item><item><title><![CDATA[Both stores said yes]]></title><description><![CDATA[A paid app, approved in the App Store and Google Play, built without opening Xcode or Android Studio.]]></description><link>https://newsletter.masilotti.com/p/ruby-native-works</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/ruby-native-works</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 28 May 2026 12:35:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1tBJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>pingrb is in the App Store and Google Play. It&#8217;s a paid app, and I built it with Ruby Native in five days without opening Xcode or Android Studio.</p><p>Anyone can upload a binary. Passing review in both stores, for a paid app, is the bar that says &#8220;this is a real app.&#8221; This is where most &#8220;we&#8217;ll get to mobile later&#8221; projects collapse, and it&#8217;s the bar Ruby Native had to clear if any of the pitch mattered.</p><p>pingrb isn&#8217;t the only one. Ender Ahmet Yurt <a href="https://rubynative.com/case-studies/foxance">shipped Foxance</a> to the App Store with Ruby Native earlier this month. Two builders, three store listings, and absolutely zero native code between the two of us.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1tBJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1tBJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1tBJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1tBJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1tBJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1tBJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5566235,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/199376793?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1tBJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1tBJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1tBJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1tBJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ae8be71-6a04-4a57-bbbf-0a05c74dec97_3600x2400.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What pingrb does</h3><p>pingrb buzzes your phone when something matters on the projects you run. A Stripe payment lands. A Hatchbox deploy fails in the middle of the night. Honeybadger catches an exception. cal.com books a meeting. I run a handful of side projects and I wanted a push notification when something worth knowing about happens. So I built the app I wanted but couldn&#8217;t find.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jz8p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jz8p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 424w, https://substackcdn.com/image/fetch/$s_!jz8p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 848w, https://substackcdn.com/image/fetch/$s_!jz8p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 1272w, https://substackcdn.com/image/fetch/$s_!jz8p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jz8p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png" width="1888" height="1275" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1275,&quot;width&quot;:1888,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:424715,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/199376793?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dbba074-970e-493e-a49f-ea9488c81488_1888x1642.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jz8p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 424w, https://substackcdn.com/image/fetch/$s_!jz8p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 848w, https://substackcdn.com/image/fetch/$s_!jz8p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 1272w, https://substackcdn.com/image/fetch/$s_!jz8p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdb87b1d-8a21-402e-9db0-c0782b7d8e73_1888x1275.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Here&#8217;s how</h3><p>Here&#8217;s pingrb&#8217;s entire native configuration:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml"># config/ruby_native.yml

tabs:
  - title: sources
    path: /sources
    icons:
      ios: list.bullet
      android: format_list_bulleted
  - title: devices
    path: /devices
    icons:
      ios: phone
      android: smartphone</code></pre></div><p>That&#8217;s it. Both platforms read the same file. The Rails app is the only place I wrote code; everything else is configuration.</p><p>For the native behaviors that need to live in a view, Ruby Native gives you Rails helpers. The navbar on the sources screen looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&lt;%= native_navbar_tag title: &#8220;Sources&#8221; do |navbar| %&gt;
  &lt;% navbar.button icon: &#8220;gearshape&#8221;, path: settings_path %&gt;
  &lt;% navbar.button icon: &#8220;plus&#8221;, path: new_source_path %&gt;
&lt;% end %&gt;</code></pre></div><p>That renders a real <code>UINavigationBar</code> on iOS and a real <code>Toolbar</code> on Android. <code>native_push_tag</code> registers the device with APNs and FCM. There are a handful of helpers like these, and they go in your ERB the same way <code>link_to</code> does.</p><p>Here&#8217;s the part that still feels weird to say out loud:</p><div class="callout-block" data-callout="true"><p>I never&#8230;</p><ul><li><p>Opened Xcode or Android Studio</p></li><li><p>Wrote a line of Swift or Kotlin</p></li><li><p>Managed signing certificates or keystores</p></li><li><p>Touched App Store Connect or Play Console more than once</p></li><li><p>Configured TestFlight or Play Closed Testing by hand</p></li></ul></div><h3>Five days end to end, one Rails developer</h3><p>Building the Rails app took a few days, which is where the actual product lives. Wiring it up to Ruby Native for iOS took three more, mostly icon-picking and getting push registration working. Android was a single morning of configuration. By that afternoon, the same Rails app was queued for review in the Play Console.</p><p>End to end, one Rails developer shipped a paid app to two stores. The &#8220;second codebase&#8221; problem doesn&#8217;t even apply, because it&#8217;s only a single codebase.</p><p>Speaking of which, the whole codebase is public. The Rails app, the Ruby Native config, and the view helpers all live at <a href="https://github.com/ruby-native/pingrb">ruby-native/pingrb on GitHub</a>.</p><h3>Try it on your own Rails app</h3><p>If you&#8217;ve been told a Rails app needs Swift, Kotlin, or a React Native rewrite to get into the stores, pingrb is the proof you don&#8217;t. So is Foxance. Two Rails developers, three store listings, in days not months.</p><p style="text-align: center;"><em>Free TestFlight builds, no credit card, and you only pay when go live.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://rubynative.com&quot;,&quot;text&quot;:&quot;Try Ruby Native free &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://rubynative.com"><span>Try Ruby Native free &#8594;</span></a></p><p>Want to kick the tires first? Install pingrb from the <a href="https://apps.apple.com/us/app/pingrb-webhook-push-alerts/id6768401822">App Store</a> or <a href="https://play.google.com/store/apps/details?id=com.pingrb.rubynative">Google Play</a>, or <a href="https://rubynative.com/case-studies/pingrb">read the full case study</a> with the YAML, helpers, and tradeoffs.</p>]]></content:encoded></item><item><title><![CDATA[I used to own a tiny niche]]></title><description><![CDATA[The code is a commodity. The decisions around it are not.]]></description><link>https://newsletter.masilotti.com/p/i-used-to-own-a-tiny-niche</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/i-used-to-own-a-tiny-niche</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 21 May 2026 13:52:51 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ce7aa731-eed0-43a3-98fa-49064fdcd554_1545x1024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends,</p><p>A few weeks ago I wrote about how <a href="https://newsletter.masilotti.com/p/a-different-kind-of-tired">the hard part of my job moved</a>. Less typing, more judgment. The work changed.</p><p>Here&#8217;s the part I didn&#8217;t say in that one: once the work changed, I had to change how I introduce myself.</p><h2><strong>The tiny niche</strong></h2><p>For years, my pitch was &#8220;I&#8217;m the Hotwire Native guy.&#8221; It was honest. I&#8217;d shipped 25+ apps on the framework. <a href="https://pragprog.com/titles/jmnative/hotwire-native-for-rails-developers/">I wrote the book.</a> I trained hundreds of developers. If you needed someone who knew Hotwire Native, it was me.</p><p>Owning a niche that small has a funny effect on your business. The leads are perfectly qualified. The conversion rate is high. Nobody else shows up in your inbox talking about the same thing. You feel like you have a moat.</p><p>You don&#8217;t.</p><h2><strong>The moat fills in</strong></h2><p>I published a book. That was good for readers and great for anyone who wanted to learn what I knew without hiring me. Fair trade. That&#8217;s the job.</p><p>I published free tutorials. Same deal.</p><p>Then AI showed up, and anyone could feed my content into Claude and generate 80% of what I used to deliver in a project. Not 100%. But 80% is enough for a lot of teams.</p><p>Turns out that moat was a puddle.</p><h2><strong>Jumping to a bigger pond</strong></h2><p>I had two options. Defend the niche by getting narrower (the Hotwire Native <em>in-app purchases</em> guy, the Hotwire Native <em>push notifications</em> guy), or expand what I sell.</p><p>I picked the second option.</p><p>I&#8217;m not the Hotwire Native guy anymore. <strong>I&#8217;m the person you call when your Rails business needs a mobile app.</strong></p><p>That shift is small on the page and enormous in practice. Hotwire Native is <em>how</em> I do the work. It&#8217;s not <em>what</em> I sell. What I sell is everything around the code: deciding whether you even need a mobile app, choosing the right framework, mapping the App Store landmines, and knowing which tradeoffs will matter a year from now.</p><p>The code is a commodity. The decisions around the code are not.</p><h2><strong>What this looks like on a call</strong></h2><p>Last month a founder asked me for a mobile build. I told him he didn&#8217;t need a mobile app yet. He needed a mobile-friendly website, a working checkout, and six more months of product-market fit before he spent a dollar on anything native.</p><p>He didn&#8217;t hire me. He also didn&#8217;t spend $40k building the wrong thing.</p><p>That conversation isn&#8217;t one I could have had as &#8220;the Hotwire Native guy.&#8221; If your whole pitch is a framework and someone asks for an app, you say yes. If your pitch is &#8220;I help Rails businesses ship mobile apps,&#8221; sometimes the right answer is &#8220;not yet.&#8221; The clients I do take end up trusting me more because of it.</p><p>And that&#8217;s the work I actually want to be doing. A decade of pattern recognition, applied before a single line gets written.</p><p>For example, the engagement I&#8217;m most energized by right now is collaborating with a team on a hybrid mobile app <em>that isn&#8217;t Hotwire Native</em>. A year ago I&#8217;d have passed. Wrong framework? No thanks! Today it&#8217;s the most fun I&#8217;ve had on a client project in a long time. They aren&#8217;t paying me for Hotwire Native. They&#8217;re paying me for the 25+ apps that came before theirs, and the judgment of which decisions matter at their stage.</p><h2><strong>Why I&#8217;m telling you this</strong></h2><p>Two reasons.</p><p>First, if you&#8217;ve been reading this newsletter for a while, the shift will show up in what I write. Honestly, it probably already has. It means more business context, more strategy, and more &#8220;should you build this.&#8221; But a little less &#8220;here&#8217;s the Hotwire Native gotcha I hit on Tuesday.&#8221; Both still matter. The mix is different now.</p><p>Second, if you run a Rails business and you&#8217;ve been putting off mobile because you&#8217;re not sure what to build, that&#8217;s the conversation I&#8217;d love to have with you. Not a code review. The one before that.</p><p>If that sounds like the conversation you need, <a href="https://cal.com/joemasilotti/mobile">book a call</a>. We&#8217;ll talk through what you&#8217;re building, what&#8217;s blocking it, and what the right next step actually is. No proposal slides, no fixed agenda. Just the conversation before the code.</p><p>And if you&#8217;re going through your own positioning shift, I&#8217;d love to hear where yours landed. Leave a comment and tell me what pond you jumped into.</p>]]></content:encoded></item><item><title><![CDATA[Ruby Native is here]]></title><description><![CDATA[Ship your Rails app to the App Store without opening Xcode. $100 off through May 18.]]></description><link>https://newsletter.masilotti.com/p/ruby-native-is-here</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/ruby-native-is-here</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Mon, 04 May 2026 13:20:53 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a69b11ec-23b5-44db-af40-68588617574c_2400x1260.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You have a Rails app. Customers keep asking for a mobile app. You start looking at options.</p><p>Rewriting in Swift and Kotlin means two new codebases your team doesn&#8217;t know. Hiring an agency means $80k, six months, and an app that drifts out of sync every sprint. WebView wrappers feel like a chrome-less browser, and review rejects them without native components. React Native and Flutter add a third codebase in a third language that still doesn&#8217;t talk to your Rails backend natively.</p><p>You&#8217;re a Rails developer. Your team ships web features in days. Why does mobile take a year?</p><p>Hotwire Native is the closest anyone has gotten. It wraps your Rails views in a native shell, with native navigation and transitions between screens. I wrote the <a href="https://pragprog.com/titles/jmnative/hotwire-native-for-rails-developers">book on it</a>. But it solves the rendering problem, not the shipping problem. You still have to open Xcode and Android Studio. You still have to learn enough Swift and Kotlin to be dangerous. And you still have to figure out everything that happens between &#8220;it runs on my simulator&#8221; and &#8220;it&#8217;s in the App Store.&#8221;</p><p>&#8220;But AI can write Swift now.&#8221; Sure. Claude will happily generate a UIKit view controller. It will not generate your Apple Developer account, your provisioning profile, your TestFlight build, or your review submission. And every time you ship a feature on the web, AI gets to write it three more times: once in Swift, once in Kotlin, once to keep the API in sync. A webview-based app gets all of that for free.</p><div class="pullquote"><p>The code was never the hard part.</p></div><p>Whether you go fully native, hire it out, or use Hotwire Native, you still need Xcode and Android Studio. Apple Developer and Google Play accounts. Signing certs and provisioning profiles. A mobile CI pipeline. App Store Connect setup, TestFlight, beta groups. Swift and Kotlin bridge components. In-app purchases wired to your backend. Fresh screenshots, metadata, and review notes every release.</p><p>That&#8217;s the wall. It&#8217;s not the rendering, <em>it&#8217;s the shipping</em>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s4HC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s4HC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 424w, https://substackcdn.com/image/fetch/$s_!s4HC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 848w, https://substackcdn.com/image/fetch/$s_!s4HC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 1272w, https://substackcdn.com/image/fetch/$s_!s4HC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s4HC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png" width="1456" height="950" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:950,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:811137,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/195477561?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!s4HC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 424w, https://substackcdn.com/image/fetch/$s_!s4HC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 848w, https://substackcdn.com/image/fetch/$s_!s4HC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 1272w, https://substackcdn.com/image/fetch/$s_!s4HC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85d2c952-6a77-44a6-a027-1fcd3581b9e8_3248x2120.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Ruby Native building and deploying the Beervana iOS app.</figcaption></figure></div><p>Ruby Native is the layer that handles everything else. You add a gem to your Rails app, configure it with a YAML file, and run <code>bundle exec ruby_native deploy</code>. We build, sign, and ship the binary to TestFlight or the App Store. Native tabs, navigation, in-app purchases, and push notifications are all driven from your Rails views with a few helper tags.</p><p><a href="https://apps.apple.com/us/app/beervana-pdx-brewery-passport/id6760509246">Beervana</a> is live on the App Store right now, and I never opened Xcode to ship it.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;a6459a3c-bd9a-418c-a469-1e808a43dd90&quot;,&quot;duration&quot;:null}"></div><p>Today Ruby Native ships iOS apps. Android is in <a href="https://rubynative.com/android-beta">active development</a> and coming next.</p><p>If you have a Rails app and customers asking for mobile then this is for you. Plans start at $299/year.</p><p><strong>Use code </strong><code>LAUNCH100</code><strong> for $100 off your first year.</strong> Expires in two weeks (May 18).</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://rubynative.com&quot;,&quot;text&quot;:&quot;Get started today&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://rubynative.com"><span>Get started today</span></a></p>]]></content:encoded></item><item><title><![CDATA[How to build a Rails app that’s ready for the app stores]]></title><description><![CDATA[The five rules I'd hand to my past self to save months of mobile-prep refactoring.]]></description><link>https://newsletter.masilotti.com/p/how-to-build-a-rails-app-thats-ready</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/how-to-build-a-rails-app-thats-ready</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Tue, 28 Apr 2026 15:19:49 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e5c73640-b74b-473e-8881-c7a936755a8f_5534x3395.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends,</p><p>A reader asked me a great question this week:</p><blockquote><p>Is there a good source for best practices in a Rails app to prepare it for going native?</p></blockquote><p>They gave two examples. One small: skipping Turbo to fix an annoying redirect on the web, which then breaks page transitions on mobile. One big: realizing his web app has a dozen top-level paths, but the mobile tab bar can only hold five.</p><p>I&#8217;ve seen this pattern in almost every Rails codebase I review for clients. The code isn&#8217;t wrong. It&#8217;s just web-first in ways that make the mobile version harder than it needs to be.</p><p>The good news: most of this is about decisions, not architecture. Make them early and you save yourself a rewrite six months in. This applies whether you ship with Hotwire Native or Ruby Native.</p><p>Here are the five rules I&#8217;d hand to my past self.</p><h2><strong>1. Design your information architecture around 3-5 entry points</strong></h2><p>Apple&#8217;s <a href="https://developer.apple.com/design/human-interface-guidelines/tab-bars">Human Interface Guidelines</a> and Google&#8217;s <a href="https://m3.material.io/components/navigation-bar/guidelines">Material guidelines</a> both recommend 3-5 tabs. In practice, this isn&#8217;t a suggestion, it&#8217;s a constraint that shapes your entire app.</p><p>Here&#8217;s what that actually looks like in practice. Say your web app has this top nav:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;9187688b-3144-4668-99a2-0333b3fff5e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Dashboard &#183; Projects &#183; Reports &#183; Teams &#183; Billing &#183; Notifications &#183; Settings &#183; Admin &#183; Help</code></pre></div><p>That&#8217;s nine items but the mobile apps can show at most five. So before you write a single controller, you have to decide which get the highest real estate. And which are demoted behind a &#8220;More&#8221; tab or link.</p><p>The constraint is a feature, not a limitation. Mobile apps work best when they&#8217;re <em>focused</em>. The web can afford to be a catch-all because users are sitting at a desk with a mouse, skimming a full nav bar. A phone user tapping through one-handed on a subway needs the app to pick a lane. The five-tab limit forces you to answer &#8220;what is this app <em>for</em>?&#8221; in a way that a web nav never does.</p><p>This exercise feels boring. But it&#8217;s the most important one you&#8217;ll do. Every URL you design, every redirect you wire up, and every nav partial you build should reinforce those five destinations.</p><p>If you skip this step and just &#8220;make the web nav work on mobile,&#8221; you&#8217;ll end up with a hamburger menu that hides everything, which is exactly the experience native users hate.</p><p><strong>The test:</strong> Can a user reach each core feature in 2 taps from the tab bar? If yes, you&#8217;ve got the right architecture.</p><h2><strong>2. Lean into Turbo from day one</strong></h2><p>The reader&#8217;s example was perfect: They disabled Turbo to fix a rendering issue on the web, which then broke mobile page transitions.</p><p>This happens constantly. A flash message doesn&#8217;t show up after a form submit, or a JavaScript library doesn&#8217;t play nice with Turbo&#8217;s fetch, so the developer reaches for the fastest fix they can think of:</p><pre><code>&lt;%= button_to &#8220;Delete&#8221;, post_path(@post), method: :delete, data: { turbo: false } %&gt;</code></pre><pre><code>&lt;%= link_to &#8220;Dashboard&#8221;, dashboard_path, data: { turbo: false } %&gt;</code></pre><p>On the web, this &#8220;works.&#8221; The flash shows, the JavaScript library loads, and the bug goes away. On mobile, this request breaks a transition or <em>is ignored entirely</em>. Hotwire Native needs Turbo to navigate. Without it, link taps silently fail.</p><p>The right move is to fix the underlying issue, not mask it. If the flash isn&#8217;t showing, respond with a Turbo Stream that updates it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:&quot;841a79f6-90b2-4d18-8ced-296421659300&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">def create
  @post = Post.new(post_params)
  if @post.save
    respond_to do |format|
      format.turbo_stream { flash.now[:notice] = &#8220;Post created&#8221; }
      format.html { redirect_to posts_path, notice: &#8220;Post created&#8221; }
    end
  else
    render :new, status: :unprocessable_entity
  end
end</code></pre></div><p>The mental model shift: <strong>Turbo isn&#8217;t a web library, it&#8217;s the navigation layer.</strong> The web just happens to be one of the clients. Reach for <code>data-turbo="false"</code> and you&#8217;re papering over a web bug at the cost of the mobile experience.</p><p><strong>The exception</strong>: The only time you want to disable Turbo navigation is with external POST requests, like Stripe checkout options. But those should be handled on their own, with fully native code. Or converted to in-app purchases.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;2b3ffd43-73cb-4c39-a443-7fedfd38d991&quot;,&quot;caption&quot;:&quot;If you&#8217;re selling digital content or subscriptions in your iOS app, Apple requires you to use in-app purchases. No way around it. And for Rails developers, this is one of the trickiest features to build: you need native Swift code to talk to StoreKit, webhook handlers to process Apple&#8217;s notifications, and a way to sync it all back to your server.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Hotwire Native: In-app Purchases on iOS&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;I help Rails developers ship iOS and Android apps. I've shipped 25+ apps, wrote the book on Hotwire Native, and share what I'm learning about running a business without burning out.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ddd4091-7614-483d-a47a-ab4a647abd44_1018x1018.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-01-22T16:05:11.026Z&quot;,&quot;cover_image&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/44cc202a-e937-4e01-b8e4-afff9110b58c_2400x1260.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/hotwire-native-deep-dive-in-app-purchases&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:183849101,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:3,&quot;comment_count&quot;:2,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Joe Masilotti&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!7JI_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ddd4091-7614-483d-a47a-ab4a647abd44_1018x1018.jpeg&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2><strong>3. Stick to RESTful, predictable URLs</strong></h2><p>Hotwire Native uses <a href="https://native.hotwired.dev/reference/path-configuration">path configuration</a> to decide how each screen should be presented. Modals, replacements, refreshes, external links&#8230; they are all pattern-matched against the URL.</p><p>Here&#8217;s a typical path configuration that relies on Rails conventions:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;d13ec347-acfa-4553-a5ae-268a2c717e5f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  &#8220;rules&#8221;: [
    {
      &#8220;patterns&#8221;: [&#8221;/new$&#8221;, &#8220;/edit$&#8221;],
      &#8220;properties&#8221;: { &#8220;context&#8221;: &#8220;modal&#8221; }
    },
    {
      &#8220;patterns&#8221;: [&#8221;/sign_in&#8221;, &#8220;/sign_up&#8221;],
      &#8220;properties&#8221;: { &#8220;presentation&#8221;: &#8220;replace_root&#8221; }
    }
  ]
}</code></pre></div><p>Those two rules handle 90% of a CRUD app. Every form shows as a modal. Auth screens replace the whole stack. Everything else pushes onto the navigation.</p><p>But this only works if your routes look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:&quot;3879dfe7-89b6-4eed-919b-8ed962efe638&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">resources :posts
resources :profiles</code></pre></div><p>Which gives you predictable URLs like <code>/posts/new</code>, <code>/posts/123/edit</code>, <code>/profiles/edit</code>. The <code>$</code> anchors in the patterns above catch them all.</p><p>Now imagine your routes look like this, which is what I actually see in production Rails apps:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:&quot;0e6b09e9-50f3-4e22-ae64-9f4f8b92523d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby"># "Friendly" auth routes
get  "signup",  to: "users#new"
post "signup",  to: "users#create"
get  "login",   to: "sessions#new"
get  "account", to: "users#edit"</code></pre></div><p>None of <code>/signup</code>, <code>/login</code>, or <code>/account</code> end in <code>/new</code> or <code>/edit</code>. They won&#8217;t match your modal pattern. The signup form pushes onto the nav stack instead of presenting as a modal. Login pushes on top of whatever screen the user was on, instead of replacing the root.</p><p>You can fix it with custom patterns:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;36687b10-1b0c-47da-b33f-852517d642ba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  &#8220;patterns&#8221;: [&#8221;/signup&#8221;, &#8220;/login&#8221;, &#8220;/account&#8221;],
  &#8220;properties&#8221;: { &#8220;context&#8221;: &#8220;modal&#8221; }
}</code></pre></div><p>But now you&#8217;re maintaining two parallel systems: Rails convention for some screens, bespoke patterns for others. Every new &#8220;friendly&#8221; URL is another rule. Every refactor risks a mobile regression.</p><p>The rule is simple: <code>resources :things</code>, every time. Your future mobile self will thank you.</p><h2><strong>4. Gate mobile-specific UI early</strong></h2><p>You will have UI that shouldn&#8217;t appear in the mobile app:</p><ul><li><p>Your web navbar (the native tab bar replaces it)</p></li><li><p>Your marketing footer</p></li><li><p>A &#8220;Download our iOS app&#8221; banner (Apple will actually <em>reject</em> apps with this!)</p></li><li><p>Cookie consent popups</p></li><li><p>Chat widgets that don&#8217;t play nice with WKWebView</p></li></ul><p>The cheap time to handle this is before you ship. The expensive time is after, when you&#8217;re untangling a view hierarchy built around a web-only layout.</p><p>Rails ships with <code>hotwire_native_app?</code> through turbo-rails. It returns <code>true</code> when the request&#8217;s User-Agent includes &#8220;Hotwire Native.&#8221; Use it from day one.</p><p>For one-off conditionals in views:</p><pre><code>&lt;% unless hotwire_native_app? %&gt;
  &lt;%= render &#8220;marketing_footer&#8221; %&gt;
&lt;% end %&gt;</code></pre><p>For larger chunks of layout, wrap whole partials:</p><pre><code>&lt;%# app/views/layouts/application.html.erb %&gt;
&lt;body&gt;
  &lt;%= render &#8220;web_header&#8221; unless hotwire_native_app? %&gt;
  &lt;%= yield %&gt;
  &lt;%= render &#8220;web_footer&#8221; unless hotwire_native_app? %&gt;
&lt;/body&gt;</code></pre><p>For dozens of scattered elements, it&#8217;s cleaner to load a mobile-only stylesheet:</p><pre><code>&lt;%# app/views/layouts/application.html.erb %&gt;
&lt;head&gt;
  &lt;%= stylesheet_link_tag &#8220;application&#8221; %&gt;
  &lt;%= stylesheet_link_tag &#8220;native&#8221; if hotwire_native_app? %&gt;
&lt;/head&gt;</code></pre><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;css&quot;,&quot;nodeId&quot;:&quot;ae910d1f-af7e-4d60-bef3-ce6b6bf2041a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-css">/* app/assets/stylesheets/native.css */
.d-hotwire-native-none {
  display: none !important;
}</code></pre></div><p>Then mark anything you want hidden:</p><pre><code>&lt;nav class=&#8221;d-hotwire-native-none&#8221;&gt;
  &lt;%# web-only nav %&gt;
&lt;/nav&gt;</code></pre><p>Now hiding a new element in the mobile app is a one-class change, not a refactor.</p><p><strong>The pattern that scales:</strong> Every time you add a web-specific element (banner, popup, footer link), ask if it should appear in the app. If not, add the class or wrap it immediately. The cost of doing this as you go is nothing. The cost of doing it in a panicked pre-launch sprint is a week.</p><h2><strong>5. Treat authentication as &#8220;signed in forever&#8221;</strong></h2><p>On the web, session cookies expire. Users sign back in. Nobody cares.</p><p>On mobile, making someone sign in every time they open the app will destroy your ratings. &#8220;I have to log in every time&#8221; is the kind of review that kills new apps. Design for <em>signed-in-forever</em> from the start.</p><p>The mobile web views persists cookies automatically, so this mostly comes down to making the server issue long-lived sessions. With Devise, programmatically check <code>remember_me</code> for mobile users:</p><pre><code>&lt;%# app/views/devise/sessions/new.html.erb %&gt;
&lt;%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %&gt;
  &lt;%= f.email_field :email %&gt;
  &lt;%= f.password_field :password %&gt;

  &lt;% if hotwire_native_app? %&gt;
    &lt;%= f.hidden_field :remember_me, value: true %&gt;
  &lt;% else %&gt;
    &lt;%= f.check_box :remember_me %&gt;
    &lt;%= f.label :remember_me %&gt;
  &lt;% end %&gt;

  &lt;%= f.submit &#8220;Sign in&#8221; %&gt;
&lt;% end %&gt;</code></pre><p>And bump the remember-me duration to something reasonable for a mobile app:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:&quot;1679e61f-58d4-4bee-bde3-374a3550ad9c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby"># config/initializers/devise.rb
config.remember_for = 1.year</code></pre></div><p>If you&#8217;re rolling your own auth (the Rails 8 generator, for example), the same principle applies. Use <code>permanent</code> when assigning the session cookie so it lasts 20 years instead of expiring with the browser session:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:&quot;044b7d38-fa00-44bc-8b27-83f41c2caf4c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">class SessionsController &lt; ApplicationController
  def create
    if user = User.authenticate_by(session_params)
      session = user.sessions.create!
      cookies.signed.permanent[:session_id] = session.id
      redirect_to root_path
    else
      redirect_to new_session_path, alert: &#8220;Invalid email or password&#8221;
    end
  end
end</code></pre></div><p>The mobile WebView persists <code>permanent</code> cookies across app launches, so once the user signs in they stay signed in until they explicitly sign out.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;de187925-41b0-4241-bc3e-bc98c7f63339&quot;,&quot;caption&quot;:&quot;Hotwire Native brings your mobile-web friendly Rails app to iOS and Android with just a few lines of code. And without any additional configuration (or even native code!) you get a lot for free.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Hotwire Native: Authentication&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;I help Rails developers ship iOS and Android apps. I've shipped 25+ apps, wrote the book on Hotwire Native, and share what I'm learning about running a business without burning out.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ddd4091-7614-483d-a47a-ab4a647abd44_1018x1018.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-10-23T13:55:24.498Z&quot;,&quot;cover_image&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/53916feb-4bea-4918-83f8-5c20373173c4_6048x4024.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/hotwire-native-deep-dive-authentication&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:174384874,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:15,&quot;comment_count&quot;:13,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Joe Masilotti&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!7JI_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ddd4091-7614-483d-a47a-ab4a647abd44_1018x1018.jpeg&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2><strong>Build like the mobile app is coming</strong></h2><p>Two years ago mobile apps were an item on the &#8220;nice to have&#8221; checklist. Today, they are almost mandatory.</p><p>AI coding assistants compress weeks of work into hours. The flip side is that you can also paint yourself into a corner just as quickly. The reader who asked the question wants to feed these rules into Claude, and that&#8217;s exactly right. The constraint isn&#8217;t typing speed anymore, it&#8217;s the decisions embedded in the code the AI generates.</p><p><a href="https://rubynative.com/">Ruby Native</a> recently launching has also shifted the calculus. It&#8217;s early, but it makes the &#8220;Rails-first, mobile second&#8221; story more real than ever. If your Rails app follows conventions, you get a mobile app nearly for free. If it doesn&#8217;t, you get a mobile app <em>eventually</em>&#8230; after a lot of refactoring.</p><p>Want to know how your Rails app scores against these five rules? That&#8217;s exactly what I do in the first week of a <a href="https://masilotti.com/services/mobile-playbook/">Mobile Playbook</a> engagement. I pull down your codebase, walk through each rule against the real code, and hand you back a concrete list of what to refactor before the mobile build starts. Plus an architecture diagram, a risk register, and a phased build plan.</p><p>Two weeks, fixed price, and you leave knowing exactly what to change. <a href="https://cal.com/joemasilotti/mobile?service=Mobile+Playbook">Book a call</a> if you want to talk through whether it&#8217;s a fit.</p>]]></content:encoded></item><item><title><![CDATA[How I built a native iOS app with Rails and one YAML file]]></title><description><![CDATA[And didn't open Xcode once.]]></description><link>https://newsletter.masilotti.com/p/how-i-built-a-native-ios-app-with</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/how-i-built-a-native-ios-app-with</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Mon, 20 Apr 2026 15:42:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/19fdfec6-69f0-4dad-bb6e-711049108f00_3840x2160.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;c5a401fe-9e73-4dc0-b8ea-61c190425890&quot;,&quot;duration&quot;:null}"></div><p><a href="https://rubynative.com">Ruby Native</a> turns a Rails app into a native iOS app. Beervana is my first attempt at building something <em>real</em> with that. It&#8217;s a native iOS app powered entirely by HTML and a YAML file living on my Rails server. Here&#8217;s how it works.</p><h2><strong>It&#8217;s a web app under the hood &#129323;</strong></h2><p>Every screen you see in Beervana is a Rails view, served over HTTPS, rendered in a <code>WKWebView</code>. There is no separate iOS codebase. There is no API. The &#8220;app&#8221; is my existing Rails app (same controllers, same ERB templates, same Turbo frames) loaded inside a native iOS shell.</p><p>That&#8217;s the part most Rails developers already understand. And historically, that&#8217;s where the story ends, because hybrid apps just&#8230;&nbsp;<em>feel off</em>. The tab bar is painted on. The back button is wrong. The forms don&#8217;t behave like iOS forms. Users smell a web app in a trench coat.</p><p>To make Beervana feel like an app an iOS user actually expects, the shell has to do more than host a web view. It has to give me <em>real native components</em>. And it has to do all of that without me leaving my Rails codebase.</p><p>Here&#8217;s how that works in Ruby Native.</p><h2><strong>Native tab bar</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B_jZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B_jZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!B_jZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!B_jZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!B_jZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B_jZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png" width="599" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:599,&quot;bytes&quot;:2339860,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/194256861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B_jZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!B_jZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!B_jZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!B_jZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F047fc91f-d4e9-43f8-822e-ab108c08216c_2160x2160.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A native UIKit tab bar with SF Symbol icons.</p><p>Drawing a tab bar in CSS is easy. The hard part is everything else: linking between tabs so a web link in the Explore tab switches to Passport without reloading, refreshing one tab&#8217;s content after a non-GET request in another (stamp a brewery in Explore and the Passport tab needs to know), and hiding the tab bar entirely when the user signs out. That state management is where most hybrid apps fall apart.</p><p>Ruby Native handles all of that. Cross-tab refresh, auto-routing, and sign-out handling are all wired up automatically, without any additional code.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">tabs:
  - title: Explore
    path: /explore
    icon: binoculars
  - title: Passport
    path: /passport
    icon: wallet.bifold
  - title: Profile
    path: /profile
    icon: person</code></pre></div><h2><strong>Sign in with Apple</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2flt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2flt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!2flt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!2flt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!2flt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2flt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png" width="600" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:600,&quot;bytes&quot;:1049235,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/194256861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2flt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!2flt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!2flt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!2flt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b842652-2e5e-49e9-a448-a01879da96cb_2160x2160.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In the video, tapping &#8220;Sign in with Apple&#8221; opens a native system sheet. Face ID runs. The sheet dismisses. You&#8217;re signed in. That&#8217;s the bar for a production iOS app.</p><p>Getting there normally means writing Swift. <code>ASWebAuthenticationSession</code> to open the browser sheet, state and nonce handling to survive the round trip, a custom URL scheme to receive the callback, and then the awkward dance of getting your Rails session cookie onto the WKWebView so the user is actually signed in when they land back in the app. None of that is fun to write, and every piece is another thing to break.</p><p>Ruby Native does the entire native side for you. Your existing OmniAuth setup still owns the provider credentials, callback, and user creation. You just tell it which path kicks off an OAuth flow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">auth:
  oauth_paths:
    - /auth/apple</code></pre></div><h2><strong>Native navigation bar</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NDfD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NDfD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!NDfD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!NDfD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!NDfD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NDfD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png" width="600" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:600,&quot;bytes&quot;:1382369,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/194256861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NDfD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!NDfD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!NDfD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!NDfD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb6598d1-8a3b-423a-800d-d497d5f5dd47_2160x2160.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The nav bar is where a lot of a web app&#8217;s functionality lives. Edit buttons, overflow menus, account actions. In the video, tapping the menu button on the Profile tab opens a native drop-down with Sign out and Delete account. Tapping Delete account fires a native iOS alert.</p><p>All of it is powered by ERB.</p><p><code>native_navbar_tag</code> gives you a UINavigationBar. <code>navbar.button</code> adds a bar button item with an SF Symbol. A block turns that button into a drop-down menu. Each menu item uses one of two verbs: <code>href</code> navigates to a path, or <code>click</code> clicks a hidden DOM element &#8212; which means your existing <code>button_to</code> with a <code>turbo_confirm</code> prompt just works, including the native iOS alert it triggers.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">&lt;%= native_navbar_tag do |navbar| %&gt;
  &lt;% navbar.button icon: &#8220;line.3.horizontal&#8221; do |menu| %&gt;
    &lt;% menu.item &#8220;Sign out&#8221;, click: &#8220;#sign-out&#8221; %&gt;
    &lt;% menu.item &#8220;Delete account&#8221;, click: &#8220;#delete-account&#8221; %&gt;
  &lt;% end %&gt;
  &lt;% navbar.button icon: &#8220;pencil&#8221;, href: edit_profile_path %&gt;
&lt;% end %&gt;</code></pre></div><h2><strong>Native forms</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4-96!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4-96!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!4-96!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!4-96!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!4-96!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4-96!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png" width="600" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:600,&quot;bytes&quot;:1013667,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/194256861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4-96!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 424w, https://substackcdn.com/image/fetch/$s_!4-96!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 848w, https://substackcdn.com/image/fetch/$s_!4-96!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!4-96!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdcfab6d7-da83-46e1-bb84-c71779aed791_2160x2160.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Forms on iOS feel different from forms on the web. The edit screen slides up as a modal sheet. The Save button lives in the navigation bar. Tapping Save disables the form and shows a native loader while the request is in flight. When it finishes, the sheet dismisses and tapping back doesn&#8217;t take the user to an empty form page.</p><p>That set of expectations has no direct equivalent on the web, which is why most hybrid forms feel off. Ruby Native closes the gap with a single tag.</p><p><code>native_form_tag</code> tells the native app this is a form page. The <code>navbar.submit_button</code> helper puts a native Save button in the nav bar that&#8217;s wired to the Rails form below it. Submit the form and the native button disables itself, shows a loader, and waits for Rails to respond. Back navigation skips the form page automatically, so the user never sees a stale draft.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">&lt;%= native_form_tag %&gt;
&#8203;
&lt;%= native_navbar_tag &#8220;Edit profile&#8221; do |navbar| %&gt;
  &lt;% navbar.submit_button %&gt;
&lt;% end %&gt;
&#8203;
&lt;%= form_with model: current_user do |form| %&gt;
  &lt;%= form.text_field :name %&gt;
&lt;% end %&gt;</code></pre></div><h2><strong>Try it</strong></h2><p>Beervana is live on the App Store today. Everything in the video and every snippet above is running in the real, shipped app.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/beervana-pdx-brewery-passport/id6760509246" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xhCc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 424w, https://substackcdn.com/image/fetch/$s_!xhCc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 848w, https://substackcdn.com/image/fetch/$s_!xhCc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 1272w, https://substackcdn.com/image/fetch/$s_!xhCc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xhCc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png" width="256" height="85.33333333333333" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:200,&quot;width&quot;:600,&quot;resizeWidth&quot;:256,&quot;bytes&quot;:29874,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://apps.apple.com/us/app/beervana-pdx-brewery-passport/id6760509246&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/194256861?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xhCc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 424w, https://substackcdn.com/image/fetch/$s_!xhCc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 848w, https://substackcdn.com/image/fetch/$s_!xhCc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 1272w, https://substackcdn.com/image/fetch/$s_!xhCc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f1a1e0-1fd7-4ee2-87cc-d95f07b9f0a8_600x200.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>And that&#8217;s the whole point of Ruby Native. I didn&#8217;t write Swift, I didn&#8217;t open Xcode, and I ship updates from my Rails repo the same way I always have. Same controllers, same ERB, same deploys.</p><p><a href="https://rubynative.com">Ruby Native</a> starts at $299 per app per year, and you can start by shipping to TestFlight for free. The build pipeline runs in the cloud, so you never install Xcode or deal with signing certificates. If you have a Rails app and you&#8217;ve been putting off the native version, come kick the tires.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://rubynative.com/try&quot;,&quot;text&quot;:&quot;Try Ruby Native for free&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://rubynative.com/try"><span>Try Ruby Native for free</span></a></p><p>Reply with questions. I&#8217;d love to hear what you&#8217;d build!</p>]]></content:encoded></item><item><title><![CDATA[A different kind of tired]]></title><description><![CDATA[Six months with AI and the code got easier. But something else got more difficult.]]></description><link>https://newsletter.masilotti.com/p/a-different-kind-of-tired</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/a-different-kind-of-tired</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Tue, 14 Apr 2026 10:32:38 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/45fc68f2-ff32-443b-85e9-258937fa9aac_4032x3024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends,</p><p>Earlier this year I was <a href="https://newsletter.masilotti.com/p/ai-makes-it-easier-to-build-the-wrong">freaked out about AI</a>. I&#8217;ve spent the months since using it constantly, on every project, at every stage. I expected to feel replaced, or at least unsettled. Instead, something underneath the work shifted.</p><div class="pullquote"><p>The hard part of my job moved. But the new version is better.</p></div><h2><strong>What I used to get tired from</strong></h2><p>A year ago, the hard days all had the same shape. I&#8217;d close the laptop with a specific flavor of tired: I couldn&#8217;t figure out how to make a thing work. Not &#8220;this took a while.&#8221; More like banging my head against a wall for hours, trying the same thing eighteen different ways, and still not knowing why it wasn&#8217;t working.</p><p>Framework fights. State bugs. Xcode rituals. That Rails method that should have returned the thing and didn&#8217;t. The weird edge case on Android where the webview loaded twice.</p><p>That was the work. Or at least, that was the part that drained me.</p><p>It mostly doesn&#8217;t happen anymore.</p><h2><strong>The feature I wouldn&#8217;t have touched last year</strong></h2><p>Last month I was scoping a feature for <a href="https://rubynative.com/">Ruby Native</a>, my new service that lets Rails developers ship an iOS app without touching native code. The feature: monitor a private build happening on GitHub Actions, validate the inbound webhooks from Apple as the binary gets processed, and surface live progress in the Rails dashboard the whole way to TestFlight.</p><p>A year ago, I would have shied away from that idea. Not because I couldn&#8217;t write the code. Because I wouldn&#8217;t have thought it was worth the investment. Inbound webhooks from Apple, signature validation, live UI updates, error states for a service where most of the errors aren&#8217;t mine? That was weeks of yak shaving for a feature most users would never see fail.</p><p>This month, the code wasn&#8217;t the problem.</p><p>The hard part was the product decisions. How do I show the user this is working? How do I show it succeeded? How do I write the error messages so they don&#8217;t have to open App Store Connect and stare at Apple&#8217;s rejection language? How do I keep them inside my app, away from Apple&#8217;s interface, as much as possible?</p><p>That&#8217;s the hard work now. And it&#8217;s much closer to the user than anything I used to spend my time on.</p><p><strong>I love it. It&#8217;s the work I thought I was doing all along.</strong></p><p>That same week I got off a client call where we spent an hour brainstorming a fully native navigation bar. Custom buttons, dynamic content, state that depends on the user. A year ago I would have steered the conversation toward phase two. This time we just ironed out what it should be and built it.</p><p>The feature ideas got bigger. The implementation got smaller. That&#8217;s the trade.</p><h2><strong>Why I&#8217;m mostly settled now</strong></h2><p>Ruby Native is my second AI-led project. On my first I spent too much of it looking over AI&#8217;s shoulder. Reading every diff. Second-guessing every suggestion. Reviewing code I didn&#8217;t need to review because I didn&#8217;t yet trust the tool to get it right.</p><p>With Ruby Native I review the critical stuff. Anything that touches signing, money, or the App Store. The rest, I mostly let run.</p><p>That change, from monitoring to trusting, is what moved me from &#8220;freaked out&#8221; in January to &#8220;mostly settled&#8221; now. It wasn&#8217;t a realization. It was reps. A few weeks of building real things and watching the tool deliver.</p><h2><strong>What I miss</strong></h2><p>I want to be honest about one thing.</p><p>I used to end the day <em>done</em>. The sheer difficulty of getting through a hard problem was a barrier that told me to close the laptop and go be a person for a while. I&#8217;d earned the break.</p><p>Now the barrier is gone. I pick up my laptop all the time because it&#8217;s so easy to just start another thing. No context switching tax. No warm-up. No twenty minutes of remembering where I left off. Just go.</p><p>The new hard thing is knowing when to stop. I&#8217;m still figuring that one out.</p><h2><strong>What it means for the work I sell</strong></h2><p>Something else has started to change, too. How I quote projects.</p><p>I&#8217;m still figuring it out, but the shape is clear. I&#8217;m billing expertise and judgment, not code output. I can build and maintain more than I could a year ago, which means the thing I&#8217;m actually charging for isn&#8217;t &#8220;how many hours the typing took.&#8221; It&#8217;s &#8220;what should be built, in what order, with which tradeoffs, to avoid which gotchas.&#8221;</p><p>That&#8217;s the work I wanted to be paid for anyway. It&#8217;s the part I&#8217;m best at.</p><h2><strong>What&#8217;s next</strong></h2><p>The hard part of my job moved. It used to be &#8220;how do I make this work?&#8221; Now it&#8217;s &#8220;what should I actually build, and how will the user feel when they use it?&#8221;</p><p>I would rather be tired from the second question than the first.</p><p>If you&#8217;re a Rails business figuring out what to build before you write a line of code, that&#8217;s exactly what the <a href="https://masilotti.com/services/mobile-playbook/">Mobile Playbook</a> is for. Two weeks, a framework recommendation, an architecture diagram, App Store gotchas mapped out, and a phased build plan you can hand to any developer. Including yourself. Including Claude.</p>]]></content:encoded></item><item><title><![CDATA[What I’m saying when nobody’s paying me]]></title><description><![CDATA[Two podcast appearances, zero polished takes, and a whole lot of thinking out loud.]]></description><link>https://newsletter.masilotti.com/p/what-im-saying-when-nobodys-paying</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/what-im-saying-when-nobodys-paying</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Fri, 10 Apr 2026 12:43:40 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d06e0b41-82c4-49a3-a891-508c8b851413_5773x3849.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>I help Rails developers ship iOS and Android apps. I&#8217;ve shipped 25+ apps, wrote the book on Hotwire Native, and share what I&#8217;m learning about running a business without burning out.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.masilotti.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Hey there,</p><p>I was on two podcasts recently. One is my own show, Permission Not Required, where Colleen and I talk about running solo businesses. The other is Technology for Humans, where I was a guest talking about consulting, AI, and going independent.</p><p>Podcasts are&#8230; well, weird. You say things out loud that you haven&#8217;t fully processed yet. Then the episode ships and those half-formed thoughts are just kinda out there.</p><p>Here are a few that stuck with me.</p><h2><strong>Nobody pays a stranger for consulting</strong></h2><p>Every client I&#8217;ve ever signed was someone who already knew me. They&#8217;d read the newsletter for a year. They&#8217;d seen me speak at a conference. They&#8217;d worked with me on a different project years ago. Or someone they trusted sent them my way. None of them were strangers when the contract showed up.</p><p>That&#8217;s the part of consulting nobody tells you when you&#8217;re starting out. It&#8217;s not a marketplace where the best portfolio wins. <strong>It&#8217;s a relationship game with a really long lead time.</strong> You&#8217;re not selling expertise. You&#8217;re selling the comfort of working with someone who already feels like a known quantity.</p><p>Which makes the next thing I&#8217;m about to say a little painful.</p><h2><strong>&#8220;I&#8217;m looking for work&#8221; posts have never worked for me</strong></h2><p>Every solo consultant has done this. You hit a dry spell, you post on X or LinkedIn, and you wait. It has never once resulted in a signed deal for me. Not once.</p><p>What does work: a conference talk, a newsletter issue, a podcast episode. But here&#8217;s the frustrating part. There&#8217;s a six-month lag and you&#8217;ll never know which one did it. <strong>I <a href="https://newsletter.masilotti.com/p/my-rails-world-keynote-is-live-on">keynoted Rails World</a> last year and the leads from that didn&#8217;t show up until early 2026.</strong></p><p>Next conference I&#8217;ll take Colleen&#8217;s advice: before I go, research who&#8217;s attending via the Slack or Discord. I&#8217;ll make a list of people I genuinely want to meet, not for a sale, just because I find their work interesting. I&#8217;ll reach out beforehand. And if I can&#8217;t find someone, I&#8217;ll DM them: &#8220;I know you&#8217;re here, want to grab coffee?&#8221;</p><p>There&#8217;s no magic lever. Just the long game played consistently (with a lot of coffee).</p><h2><strong>The feast-or-famine cycle</strong></h2><p>I was honest about a typical year for me on the pod: Q1 is dry. Q2 brings leads. Q3 makes most of the money. Q4 goes quiet. I&#8217;m stressed for six months, then two clients sign at the same time.</p><p><strong>If I could snap my fingers and change one thing about consulting, it would be stability.</strong> Not higher revenue, just steadier revenue.</p><p>My attempt at a fix this year is saying yes to smaller advisory retainers I used to turn down. A team that wants me on call for architecture questions. A maintenance engagement where I keep an app healthy month to month. These used to feel too small to bother with. Now they&#8217;re the foundation. They don&#8217;t replace the big projects, but they keep the lights on. Stability over spikes.</p><h2><strong>What I&#8217;m actually learning right now</strong></h2><p>Here&#8217;s the part I haven&#8217;t said on any podcast yet.</p><p>I have three types of client work going on right now. A maintenance retainer where I keep an app healthy. An advisory retainer where I collaborate with a team on their native app strategy. And a build project where I&#8217;m shipping an Android app from scratch.</p><p>The build project pays the most. <strong>But the advisory and maintenance work? That&#8217;s what I love.</strong> The collaboration. Helping a team make better decisions without having to write every line of code myself. Being a <em>real part</em> of something.</p><p>I don&#8217;t know what that means long term. More of this but rebranded? Something else entirely? I&#8217;m honestly not sure.</p><p>But figuring that out isn&#8217;t urgent. Yet. The urgent thing is making this year work. I have parental leave coming up, and the difference between stressed parents and relaxed parents going into leave is what happens in the next few months. That&#8217;s my North Star right now.</p><h2><strong>Listen to both episodes</strong></h2><p>If any of this resonated, here are the full conversations:</p><ul><li><p><a href="https://nopermission.fm/nobody-pays-a-stranger-for-consulting/">Permission Not Required, Episode 9</a> with Colleen</p></li><li><p><a href="https://www.youtube.com/watch?v=r5IEdbdDTaM">Technology for Humans</a> with Errol from reinteractive</p></li></ul><p>I&#8217;d love to hear what landed for you. Drop a comment and let me know what you&#8217;re thinking!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/p/what-im-saying-when-nobodys-paying/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.masilotti.com/p/what-im-saying-when-nobodys-paying/comments"><span>Leave a comment</span></a></p>]]></content:encoded></item><item><title><![CDATA[On building a framework-agnostic Ruby gem (and making sure it doesn’t break)]]></title><description><![CDATA[What I learned supporting ERB, React, and Vue from a single Ruby gem. And how I keep it from breaking with automated iOS tests.]]></description><link>https://newsletter.masilotti.com/p/on-building-a-framework-agnostic</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/on-building-a-framework-agnostic</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 01 Apr 2026 18:32:09 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/55d3362c-4568-4b33-baba-4fff3ef408c3_1999x1306.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://rubynative.com/">Ruby Native</a> needs to work with ERB, React, and Vue. That&#8217;s three frameworks, three sets of conventions, and three ways developers expect an API to feel. And every new feature needs to ship across all of them without breaking the others.</p><p>This week <a href="https://x.com/joemasilotti/status/2039043585585406170?s=20">I added a native navbar</a>, and it was the first real test of whether this approach scales. Here&#8217;s what I&#8217;ve learned about keeping an API clean across frameworks, and how I&#8217;m catching regressions.</p><h2><strong>It&#8217;s HTML all the way down</strong></h2><p>When I started Ruby Native, it was ERB-only. The tab bar, push notifications, and forms all used the same pattern: render a hidden HTML element with <code>data-native-*</code> attributes. The native app detects these &#8220;signal elements&#8221; via a MutationObserver and translates them into real native UI.</p><p>For example, the native navbar is just this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;div data-native-navbar="Account" hidden&gt;
  &lt;div data-native-button data-native-icon="ellipsis.circle"&gt;
    &lt;div data-native-menu-item data-native-title="Edit profile"
         data-native-href="/account/edit" data-native-icon="pencil"&gt;
    &lt;/div&gt;
    &lt;div data-native-menu-item data-native-title="Sign out"
         data-native-click="#sign-out" data-native-icon="rectangle.portrait"&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre></div><p>Hidden divs, that&#8217;s it! The native app doesn&#8217;t know what generated them. It just reads the DOM.</p><p>This turned out to be the most important decision in the entire project. Because when it came time to support React and Vue, I didn&#8217;t have to change anything on the native side. <em>I just needed new ways to produce the same HTML.</em></p><h2><strong>Each framework&#8217;s API should feel </strong><em><strong>good</strong></em></h2><p>ERB developers expect blocks and builders. React developers expect components and props. If you force one framework&#8217;s patterns onto another, the API feels wrong even if it works.</p><p>Here&#8217;s the same navbar in ERB:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&lt;%= native_navbar_tag "Account" do |navbar| %&gt;
  &lt;% navbar.button icon: "ellipsis.circle" do |menu| %&gt;
    &lt;% menu.item "Edit profile", href: "/account/edit", icon: "pencil" %&gt;
    &lt;% menu.item "Sign out", click: "#sign-out", icon: "rectangle.portrait" %&gt;
  &lt;% end %&gt;
&lt;% end %&gt;</code></pre></div><p>And in React:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;jsx&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-jsx">import { NativeNavbar, NativeButton, NativeMenuItem } from "ruby_native/react"

&lt;NativeNavbar title="Account"&gt;
  &lt;NativeButton icon="ellipsis.circle"&gt;
    &lt;NativeMenuItem title="Edit profile" href="/account/edit" icon="pencil" /&gt;
    &lt;NativeMenuItem title="Sign out" click="#sign-out" icon="rectangle.portrait" /&gt;
  &lt;/NativeButton&gt;
&lt;/NativeNavbar&gt;</code></pre></div><p>Different syntax. Same output. The ERB version uses Ruby&#8217;s block pattern with a builder that yields menu items. The React version uses component composition with props. Both feel right in their own context.</p><p>The React components themselves are intentionally thin. Here&#8217;s <code>NativeButton</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">export function NativeButton({ position = "trailing", icon, title, href, click, selected, children }) {
  const props = { "data-native-button": true }
  if (icon) props["data-native-icon"] = icon
  if (title) props["data-native-title"] = title
  if (href) props["data-native-href"] = href
  if (click) props["data-native-click"] = click
  if (position) props["data-native-position"] = position
  if (selected) props["data-native-selected"] = ""
  return createElement("div", props, children)
}</code></pre></div><p>No state or effects, just a function that turns props into data attributes. The less framework-specific logic lives in these components, the less can go wrong.</p><h2><strong>I don&#8217;t use Inertia day-to-day, and it shows! &#128556;</strong></h2><p>I&#8217;m a Rails and ERB developer. I don&#8217;t reach for React or Vue on my own projects. When I built the Inertia support, I leaned heavily on early Ruby Native adopters who use Inertia every day. They told me when something felt off, when a prop name was confusing, or when a pattern didn&#8217;t match how they structure their apps.</p><p>This is something I think library authors underestimate. You can read the docs and follow the conventions, but there&#8217;s a <em>feel</em> to a framework that only comes from daily use. Having people who live in that framework test your API is the difference between &#8220;technically works&#8221; and &#8220;feels right.&#8221;</p><h2><strong>Three frameworks from one test bundle</strong></h2><p>Supporting multiple frameworks creates a real regression problem. A change to the JavaScript bridge could break React but not ERB. A new signal element might work in Hotwire but lead to a race condition in Inertia. And I don&#8217;t want to manually check every combination on every change.</p><p>So I set up XCUITest tests for each of the <a href="https://github.com/ruby-native/demos">three demo apps</a>: Beervana (Hotwire/ERB), Coffee (React/Inertia), and Habits (Vue/Inertia). Each test suite boots the real Rails server and exercises the actual native UI.</p><p>The tests don&#8217;t assert on HTML or JavaScript. They assert on what the user sees:</p><ul><li><p>Does the tab bar appear after sign-in?</p></li><li><p>Does the navbar title update when I switch tabs?</p></li><li><p>Does the menu button open with the right items?</p></li></ul><p>Here&#8217;s what the Coffee (React) test looks like for the navbar menu:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// Menu button: Account page has an ellipsis button with menu items.
let menuButton = app.navigationBars.buttons["More"]
XCTAssertTrue(menuButton.waitForExistence(timeout: 3))
menuButton.tap()

let editAction = app.buttons["Edit profile"]
XCTAssertTrue(editAction.waitForExistence(timeout: 3))</code></pre></div><p>And the equivalent Beervana (Hotwire) test for tab navigation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">app.tabBars.buttons["Passport"].tap()
XCTAssertTrue(app.navigationBars["Passport"].waitForExistence(timeout: 5))

app.tabBars.buttons["Profile"].tap()
XCTAssertTrue(app.navigationBars["Profile"].waitForExistence(timeout: 5))</code></pre></div><p>Both tests are framework-agnostic. They have no idea what&#8217;s rendering the HTML. They just verify the <em>native UI</em> works. If I break something in the JavaScript bridge, these tests catch it regardless of which framework exposed the problem.</p><h2><strong>And then someone asked about Sinatra&#8230; &#128557;</strong></h2><p>I haven&#8217;t tried it yet, but someone recently asked if Ruby Native works outside of Rails. With Sinatra, for example. And the honest answer is: it should. The signal elements are just HTML. The native app reads the DOM. There&#8217;s nothing Rails-specific about the data attributes themselves.</p><p>The Ruby gem&#8217;s helpers are Rails-specific, sure. But the React and Vue components aren&#8217;t. And you could render the raw HTML from any templating language.</p><p>That question made me realize the early decision to build on data attributes rather than framework hooks is paying off in ways I didn&#8217;t plan for. It&#8217;s one more framework to think about. But the fact that it&#8217;s even plausible tells me the abstraction is at the right layer.</p><div><hr></div><p>If you&#8217;re using Inertia with Rails and want to try <a href="https://rubynative.com">Ruby Native</a>, I&#8217;d love your feedback. The people who use these frameworks daily are the ones who make the API better.</p><div class="directMessage button" data-attrs="{&quot;userId&quot;:16209564,&quot;userName&quot;:&quot;Joe Masilotti&quot;,&quot;canDm&quot;:null,&quot;dmUpgradeOptions&quot;:null,&quot;isEditorNode&quot;:true}" data-component-name="DirectMessageToDOM"></div>]]></content:encoded></item><item><title><![CDATA[Why I ditched monthly pricing]]></title><description><![CDATA[Ruby Native isn't a traditional SaaS. So it shouldn't be priced like one.]]></description><link>https://newsletter.masilotti.com/p/why-i-ditched-monthly-pricing</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/why-i-ditched-monthly-pricing</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 26 Mar 2026 12:24:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d54d91bd-dc2a-4b54-9167-fe48df9f0c7b_2884x1600.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I <a href="https://open.substack.com/pub/joemasilotti/p/the-tool-i-wish-i-had-25-apps-ago">first announced Ruby Native</a>, I priced it like every other SaaS tool I&#8217;d ever built: monthly subscriptions at three different tiers, renewable forever.</p><p>It felt right! And it&#8217;s what developers expect. It&#8217;s the &#8220;proven&#8221; model.</p><p>Then I sat down and asked myself an uncomfortable question: <em>What do customers actually get from Ruby Native month after month?</em></p><p>And I didn&#8217;t love the answer.</p><h2><strong>The problem with monthly recurring value</strong></h2><p><a href="https://rubynative.com/">Ruby Native</a> lets Rails developers ship native iOS apps without leaving Ruby. The pitch is: write Ruby, get a native app.</p><p>Here&#8217;s the thing about that native app once it&#8217;s in the App Store: most of it is your Rails app rendered in a web view. You push a change to your server, the mobile app picks it up automatically. No rebuild or App Store review. And Ruby Native isn&#8217;t involved at all.</p><p>So what does month three of a Ruby Native subscription actually buy you? Or month six? If your app is shipped and working, the answer is: not much. You&#8217;re not building on Ruby Native&#8217;s infrastructure. You&#8217;re building on <em>yours</em>.</p><p>That&#8217;s a rough foundation for a monthly subscription business. You&#8217;re essentially charging people a recurring fee for a tool they used intensively for a few weeks and now barely touch.</p><p>I could have glossed over this with more features and more integrations. More more more! In the age of agentic coding&#8230; that feels possible. Right?</p><p>But I&#8217;m a solo operator. I&#8217;d rather be honest about what the product actually delivers than manufacture reasons for monthly churn anxiety.</p><h2><strong>What changes when you switch to annual licensing</strong></h2><p>The new model flips the equation to match reality:</p><p><strong>Repo access is yours forever</strong> once you&#8217;ve held an active license. Ship the app, let the subscription lapse. The code you built on doesn&#8217;t disappear.</p><p><strong>Cloud builds and iOS compatibility updates</strong> require an active subscription. Apple changes things constantly. New Xcode versions, new SDK requirements, new App Store rules. Staying current is ongoing work, and that&#8217;s what an active license covers.</p><p><strong>Your app stays in the App Store</strong> even if your subscription expires. You just have to build and deploy manually instead of using Ruby Native&#8217;s cloud infrastructure.</p><p>This maps to actual value. Paying while you&#8217;re actively building or need to stay iOS-current makes sense. Paying because you forgot to cancel doesn&#8217;t.</p><h2><strong>The tiers</strong></h2><p>I settled on just two tiers, priced annually:</p><ul><li><p><strong>STARTER at $299/year</strong>: Cloud builds, repo access, email support. For developers who want to ship and stay current without hand-holding.</p></li><li><p><strong>PRO at $999/year</strong>: Adds in-app purchases and priority support. For apps that need to make money.</p></li></ul><p>I also dropped MAU limits entirely. Usage caps on a product that runs on your infrastructure never made sense anyway. It was legacy SaaS thinking applied to a tool that doesn&#8217;t work like SaaS.</p><h2><strong>The trade-off I made</strong></h2><p>Annual licensing means lower theoretical LTV than monthly if a customer sticks around for years. I&#8217;m aware of that.</p><p>But monthly pricing for a product with thin recurring value creates a different problem: customers who feel vaguely guilty every month, who churn when they get busy, who resent the charge when they&#8217;re not actively building. That&#8217;s a bad relationship to be in.</p><p>Annual licensing sets honest expectations. You pay for a year, you get a year of cloud builds and iOS updates. If that&#8217;s worth it, renew. If not, your app still works. And there&#8217;s no lock in if you want to use the code as a starting point.</p><p>I&#8217;d rather charge a fair price for what I actually deliver than optimize for a revenue model that doesn&#8217;t fit the product.</p>]]></content:encoded></item><item><title><![CDATA[The tool I wish I had 25 apps ago]]></title><description><![CDATA[Ruby Native turns your Rails app into an iOS app. No Xcode required.]]></description><link>https://newsletter.masilotti.com/p/the-tool-i-wish-i-had-25-apps-ago</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/the-tool-i-wish-i-had-25-apps-ago</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 18 Mar 2026 12:12:35 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/93a3f9c2-2f78-4bbb-b668-a76408c80872_2400x1260.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every app I&#8217;ve shipped over the last nine years started the same way.</p><p>A Rails developer reaches out with a great web app. Users love it. But they want to be in the App Store and they have no idea where to start.</p><p>So I help them set up Xcode. We configure signing certificates, provisioning profiles, and entitlements. We write Swift to handle navigation, tabs, push notifications, and authentication. We debug build errors that have nothing to do with their actual product.</p><p>It always works. And it always takes longer than it feels like it should. But the hardest part is never the app itself. It&#8217;s the tooling around it.</p><p>After 25+ apps, I started to notice something. A good chunk of every app I build is the same. The tab bar. The navigation. The configuration. The bridge components. The build pipeline&#8230; I was solving the same problems over and over, just for different clients.</p><p>So I started pulling those pieces into a single tool. And I kept going until the tool could do something I&#8217;ve wanted for a long, long time: <strong>ship a Rails app to the App Store without ever opening Xcode.</strong></p><p>And that tool, my friends, is <a href="https://rubynative.com/">Ruby Native</a>.</p><h2><strong>What it actually does</strong></h2><p>You add a gem to your Rails app. You write a YAML file that defines your app name, colors, and tabs. You run a command, scan a QR code, and see your app running natively on your iPhone. The whole thing takes about ten minutes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V5oo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V5oo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 424w, https://substackcdn.com/image/fetch/$s_!V5oo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 848w, https://substackcdn.com/image/fetch/$s_!V5oo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 1272w, https://substackcdn.com/image/fetch/$s_!V5oo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V5oo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png" width="1235" height="832" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:832,&quot;width&quot;:1235,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:275517,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/191159024?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!V5oo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 424w, https://substackcdn.com/image/fetch/$s_!V5oo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 848w, https://substackcdn.com/image/fetch/$s_!V5oo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 1272w, https://substackcdn.com/image/fetch/$s_!V5oo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e8f4ea0-6946-490f-bead-274fd7833503_1235x832.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Ruby Native configuring an iOS app with YAML.</figcaption></figure></div><p>That YAML becomes a real native tab bar. Not a web recreation. A native tab bar using first-party Apple APIs.</p><p>When you&#8217;re ready to ship, Ruby Native handles code signing, builds, and App Store submission in the cloud. You never install Xcode. You never write Swift. You deploy your Rails app like you always have, and the native shell just works.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aq0v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aq0v!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 424w, https://substackcdn.com/image/fetch/$s_!aq0v!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 848w, https://substackcdn.com/image/fetch/$s_!aq0v!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!aq0v!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aq0v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png" width="1456" height="1135" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1135,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:518849,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/191159024?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aq0v!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 424w, https://substackcdn.com/image/fetch/$s_!aq0v!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 848w, https://substackcdn.com/image/fetch/$s_!aq0v!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!aq0v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc87af291-f065-43c6-8e4d-1a2dc0acc711_2022x1576.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Why I&#8217;m excited about this</strong></h2><p><strong>It works with any frontend.</strong> Hotwire, React, Inertia, plain ERB. If your website is powered by Rails then it works with Ruby Native. You don&#8217;t have to adopt a specific framework or rewrite anything.</p><p><strong>It turns a config file into a native app.</strong> Change a color in YAML, hit refresh, see it on your phone. That feedback loop is something I never had when building apps by hand.</p><p><strong>It removes the scariest part.</strong> Most Rails developers I talk to aren&#8217;t afraid of building a good product. They&#8217;re afraid of Xcode, certificates, and the App Store submission process. Ruby Native takes all of that off their plate.</p><p>Run <code>bundle exec ruby_native preview</code> and a QR code pops up right in your terminal. Scan it with the Ruby Native Preview app on your iPhone and your Rails app is running natively on your phone. No build step, simulator, or Mac required.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hQZX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hQZX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 424w, https://substackcdn.com/image/fetch/$s_!hQZX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 848w, https://substackcdn.com/image/fetch/$s_!hQZX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 1272w, https://substackcdn.com/image/fetch/$s_!hQZX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hQZX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png" width="1456" height="912" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:912,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:103341,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/191159024?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hQZX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 424w, https://substackcdn.com/image/fetch/$s_!hQZX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 848w, https://substackcdn.com/image/fetch/$s_!hQZX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 1272w, https://substackcdn.com/image/fetch/$s_!hQZX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0914359-1ca1-4900-a9b5-a75771b31b86_2704x1694.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Ruby Native running the preview command.</figcaption></figure></div><p><em>I mean, how cool is that?!</em></p><h2><strong>Where this is headed</strong></h2><p>I&#8217;m launching Ruby Native publicly on April 1. (Not a joke, I promise. &#128517;) Right now I&#8217;m getting early users set up and working through feedback.</p><p>If you want to see what it looks like, you can <a href="https://rubynative.com/try">try it in about ten minutes</a>. No account required.</p><p>I&#8217;ll be back in a few weeks with the full launch details. For now, I just wanted to share why this exists. It came from nine years of doing the same work and finally asking, &#8220;What if the Rails developer never had to leave Rails?&#8221;</p><p>Turns out, they don&#8217;t.</p>]]></content:encoded></item><item><title><![CDATA[Quick survey + office hours on Thursday]]></title><description><![CDATA[Help me write better stuff for you!]]></description><link>https://newsletter.masilotti.com/p/quick-survey-office-hours-on-thursday</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/quick-survey-office-hours-on-thursday</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Tue, 10 Mar 2026 13:26:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7JI_!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ddd4091-7614-483d-a47a-ab4a647abd44_1018x1018.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I want to make sure I&#8217;m writing about the things that actually help you. I put together a short survey (4 questions, takes about a minute) so I can get a better sense of who&#8217;s reading and what you want more of.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/survey/6356526?token=&quot;,&quot;text&quot;:&quot;Take the Survey&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.masilotti.com/survey/6356526?token="><span>Take the Survey</span></a></p><p>Also, <a href="https://newsletter.masilotti.com/p/office-hours">I&#8217;m hosting office hours on Thursday for paid subscribers</a>. Bring whatever you&#8217;re working on. Hotwire Native questions, app architecture decisions, getting something into the App Store&#8230; whatever&#8217;s on your mind.</p><p>Thanks for reading, it means a lot!</p>]]></content:encoded></item><item><title><![CDATA[The kid at the co-working space]]></title><description><![CDATA[My son got a day off from school. I got a reminder of why I chose this life.]]></description><link>https://newsletter.masilotti.com/p/the-kid-at-the-co-working-space</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/the-kid-at-the-co-working-space</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 04 Mar 2026 20:10:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_UWU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_UWU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_UWU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_UWU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_UWU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_UWU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_UWU!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg" width="1200" height="900" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:4772504,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/189915096?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_UWU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_UWU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_UWU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_UWU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7a3ab10-327f-4159-9c7d-091ed8786efc_5712x4284.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Headphones on, toys out, totally unbothered by my failing test suite.</figcaption></figure></div><p>The other day I took my four-year-old to my co-working space.</p><p>I had him all set up. Duplos. An Etch A Sketch. Headphones with a story or two queued up. <s>A snack</s> Multiple snacks.</p><p>I figured I&#8217;d get maybe 15 minutes in before things fell apart.</p><p>I got 45.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3P1M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3P1M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 424w, https://substackcdn.com/image/fetch/$s_!3P1M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 848w, https://substackcdn.com/image/fetch/$s_!3P1M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!3P1M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3P1M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg" width="507" height="675.8839285714286" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1941,&quot;width&quot;:1456,&quot;resizeWidth&quot;:507,&quot;bytes&quot;:3148052,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/189915096?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3P1M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 424w, https://substackcdn.com/image/fetch/$s_!3P1M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 848w, https://substackcdn.com/image/fetch/$s_!3P1M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!3P1M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26398f5a-19da-488a-96b1-5e9900813f79_5712x4284.jpeg 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">He must have grabbed my phone at some point because I found this masterpiece a few days later!</figcaption></figure></div><p>He walked around the space like he owned it. Got kombucha from the self-serve station. Grabbed mints from the front desk. Sat back down and built something with Duplos while I knocked out a client email and reviewed a PR.</p><p>And I sat there thinking: if he&#8217;s home sick one day, this might actually work for a little bit.</p><p>That&#8217;s such a small thing. But when you&#8217;re independent and a parent, those small things are the whole game. There&#8217;s no calling in to your manager. There&#8217;s no backup. There&#8217;s just you, figuring out how to get 45 minutes when you need them.</p><p>I&#8217;ve been independent for long enough that I&#8217;ve stopped romanticizing it. The freedom is real, but so is the weight. Every hour I&#8217;m not working is an hour I chose not to work, and that choice is never fully clean. There&#8217;s always a client waiting, a newsletter to write, a product to ship.</p><p>And now, an agent waiting for their next prompt.</p><p>But sitting in that co-working space, watching my kid drink kombucha and hand out mints to strangers, I remembered why I do this. Not for the flexibility in the abstract. But for the flexibility in the specific. For that day, specifically.</p><p>I don&#8217;t have a lesson here. I just wanted to share it.</p><p>What&#8217;s your version of this? The small moment that reminded you why you went independent, or why you want to. Drop a comment. I&#8217;d love to hear it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/p/the-kid-at-the-co-working-space/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.masilotti.com/p/the-kid-at-the-co-working-space/comments"><span>Leave a comment</span></a></p><p></p>]]></content:encoded></item></channel></rss>