<?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>Sun, 03 May 2026 09:35:00 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[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;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;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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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 role="img" 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><title></title><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><item><title><![CDATA[Getting your Hotwire Native app into the App Store]]></title><description><![CDATA[A practical, step-by-step guide to submitting your first Hotwire Native app. From setting up your developer account to hitting "Submit for Review."]]></description><link>https://newsletter.masilotti.com/p/getting-your-hotwire-native-app-into</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/getting-your-hotwire-native-app-into</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 26 Feb 2026 16:08:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ad582d03-9f38-4797-878d-ca73fb1a4e9f_2400x1260.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends,</p><p>The hardest part of shipping a Hotwire Native app isn&#8217;t the code. It&#8217;s navigating Apple&#8217;s submission process for the first time.</p><p>There are a surprising number of gotchas that can cost you days or even weeks if you don&#8217;t know about them upfront. A DUNS number that can take a month to verify. An encryption compliance checkbox that silently holds up your TestFlight build. A rejection for &#8220;minimum functionality&#8221; because you didn&#8217;t make the app feel native enough.</p><p>I&#8217;ve shipped 25+ apps through this process (<a href="https://newsletter.masilotti.com/p/what-ive-learned-from-shipping-25?r=9nfdo">here&#8217;s what I&#8217;ve learned</a>). This guide covers every step from setting up your developer account to hitting &#8220;Submit for Review.&#8221;</p><div><hr></div><p><em>I&#8217;m Joe Masilotti, author of <a href="https://amzn.to/499Vf0G">Hotwire Native for Rails Developers</a>. This newsletter is where I share weekly thoughts on mobile app development, indie consulting, and doing great work while staying present at home.</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><h2><strong>Set up an Apple Developer account</strong></h2><p>Before you write a single line of code, <a href="https://developer.apple.com/programs/enroll/">enroll in the Apple Developer Program</a>. This might sound obvious, but I&#8217;m putting it first for a reason: it can take <em>weeks</em> to get approved.</p><p>Enroll as an <strong>organization</strong>, not an individual. It&#8217;s better for credibility in the App Store, and it makes things easier if you ever need to transfer the app to another team or company.</p><p>Here&#8217;s the catch: organizations need a DUNS number, which is a unique business identifier from Dun &amp; Bradstreet. Apple uses it to verify your company. And the verification process can drag on for a while. I had a client with 100% working code, ready to submit, who couldn&#8217;t get into the App Store for <em>months</em> because of an issue with their DUNS number. It delayed the entire launch.</p><p>Start the enrollment process as early as possible so it&#8217;s ready when you are.</p><h2><strong>Point to your production server</strong></h2><p>This one seems obvious, but it&#8217;s easy to forget when you&#8217;ve been developing locally for weeks. Swap out <code>http://localhost:3000</code> for your production URL. Or, have your app <a href="https://gist.github.com/joemasilotti/ed002068cc1239d5e799fae1e4038386">do it automatically</a> based on which environment it was installed from.</p><p>Make sure your production server is live and accessible before you submit. Apple&#8217;s reviewers will launch your app and try to use it. If it can&#8217;t reach your server, that&#8217;s an instant rejection.</p><h2><strong>Include an app icon</strong></h2><p>You only need a single 1024x1024 image. In Xcode, delete all the other size slots in the asset catalog and your one image applies everywhere.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ibrM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ibrM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 424w, https://substackcdn.com/image/fetch/$s_!ibrM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 848w, https://substackcdn.com/image/fetch/$s_!ibrM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 1272w, https://substackcdn.com/image/fetch/$s_!ibrM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ibrM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png" width="1456" height="911" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:911,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:407018,&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/188317177?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.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_!ibrM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 424w, https://substackcdn.com/image/fetch/$s_!ibrM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 848w, https://substackcdn.com/image/fetch/$s_!ibrM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.png 1272w, https://substackcdn.com/image/fetch/$s_!ibrM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ddc254e-70f7-45ce-98f0-5e774a50656c_2294x1436.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 role="img" 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><title></title><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>No need to design each version at each size. If you have a clean logo at that resolution, you&#8217;re set.</p><h2><strong>Test with TestFlight before submitting</strong></h2><p>TestFlight is Apple&#8217;s beta testing platform, built right into App Store Connect. Use it.</p><p><strong>Internal testing</strong> lets up to 100 members of your team install builds immediately after processing. No review needed. This is your first line of defense for catching obvious issues on real devices.</p><p><strong>External testing</strong> opens it up to 10,000 testers, but requires a quick beta review from Apple before the build is available.</p><p>Start with internal testing. Share the TestFlight link with your team, get real feedback on real devices. The Simulator is great for development, but there&#8217;s no substitute for tapping through your app on an actual phone.</p><p>I can&#8217;t tell you how many times I&#8217;ve caught a native- or bridge-component-integration bug when running the app on my physical iPhone for the first time.</p><h2><strong>Skip the encryption compliance wait</strong></h2><p>Speaking of TestFlight, make sure to add <code>ITSAppUsesNonExemptEncryption</code> = <code>NO</code> to your Info.plist.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nLOr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nLOr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 424w, https://substackcdn.com/image/fetch/$s_!nLOr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 848w, https://substackcdn.com/image/fetch/$s_!nLOr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 1272w, https://substackcdn.com/image/fetch/$s_!nLOr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nLOr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png" width="1456" height="573" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:573,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:263284,&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/188317177?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.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_!nLOr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 424w, https://substackcdn.com/image/fetch/$s_!nLOr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 848w, https://substackcdn.com/image/fetch/$s_!nLOr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.png 1272w, https://substackcdn.com/image/fetch/$s_!nLOr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9d99aa3-2f29-4815-a810-5bae441b22b5_1880x740.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 role="img" 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><title></title><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>Without this, every build you upload to TestFlight sits in &#8220;Processing&#8221; waiting for you to manually answer export compliance questions in App Store Connect. It&#8217;s a silent blocker. Your build will look like it&#8217;s still processing and you&#8217;ll be wondering what went wrong.</p><p>Unless you&#8217;re doing something custom with encryption beyond standard HTTPS, set this to <code>NO</code>.</p><p><em>The rest of this post covers: creating App Store screenshots (with and without a designer), keyword optimization tricks, privacy and review questionnaire gotchas, how to avoid Apple&#8217;s &#8220;minimum functionality&#8221; rejection, and a post-launch tip that saves you 15% on every in-app purchase.</em></p>
      <p>
          <a href="https://newsletter.masilotti.com/p/getting-your-hotwire-native-app-into">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[What I've learned from shipping 25+ mobile apps]]></title><description><![CDATA[And what I tell my clients when they ask which framework to use for their Rails business.]]></description><link>https://newsletter.masilotti.com/p/what-ive-learned-from-shipping-25</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/what-ive-learned-from-shipping-25</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 19 Feb 2026 14:46:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d9721ad4-004f-41d9-8175-44f5e18c4507_2580x1382.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends, I recently published a new article on my website: <a href="https://masilotti.com/rails-developers-guide-to-mobile-app-frameworks/">The Rails developers&#8217; guide to mobile app frameworks</a>. It covers fully native (Swift/Kotlin), React Native, PWAs, and Hotwire Native with a comparison table and recommendations for when to pick each.</p><p>But I didn&#8217;t want to just send you a link. The article is the balanced, &#8220;here are your options&#8221; version. What you&#8217;re reading is the <em>opinionated</em> version. The stuff I&#8217;ve learned from actually building these apps, not just evaluating frameworks.</p><h2><strong>Most teams overestimate how much &#8220;native&#8221; they need</strong></h2><p>This is the pattern I see more than any other. A team comes to me convinced they need custom native screens for half their app. They want native forms, native lists, native everything. They&#8217;ve been burned by web views that look janky and they don&#8217;t want that experience.</p><p><strong>Then we look at what their users actually do. And 80-90% of it is standard CRUD.</strong> Creating records, viewing lists, editing profiles, managing settings. All of that works beautifully as server-rendered HTML inside a native navigation wrapper. No custom Swift or Kotlin needed.</p><p>The native parts that matter are the ones that <em>can&#8217;t</em> be web. Push notifications, camera access, biometrics, haptic feedback, in-app purchases. Those need to be native. And most of them are doable with <a href="https://masilotti.com/bridge-components/">bridge components</a> in a few lines of HTML!</p><h2><strong>The apps that struggle try to do too much natively</strong></h2><p>When I look at Hotwire Native projects that have gone sideways, the pattern is almost always the same: they tried to build too many custom native screens too early.</p><p><strong>Every screen you build natively is a screen you now maintain in three places.</strong> The data model in Rails, the API endpoint, and the native UI in both Swift and Kotlin. That&#8217;s fine for two or three critical screens. It&#8217;s a nightmare for fifteen.</p><p>The best Hotwire Native apps I&#8217;ve worked on keep the native layer thin. They lean heavily on server-rendered HTML and only &#8220;drop down to native&#8221; when there&#8217;s a clear reason. That discipline is what keeps the maintenance burden manageable for a small team.</p><h2><strong>The React Native question</strong></h2><p>This is the one I get asked most. &#8220;Should we just use React Native?&#8221;</p><p>My honest answer: <strong>if your team is already writing React on the web and you have a mature API, React Native is a solid choice</strong>. I won&#8217;t pretend otherwise. You never have to open Xcode or Android Studio, the plugin ecosystem is huge, and the developer ergonomics are genuinely good.</p><p>But that&#8217;s rarely the situation I see. Most Rails teams are using ERB, Hotwire, and Turbo. They don&#8217;t have a React frontend. They don&#8217;t have a robust JSON API. Choosing React Native means building both of those from scratch, on top of learning the framework itself.</p><p>And here&#8217;s the part that surprises people: React web and React Native are different enough that you can&#8217;t share frontend code between them. You&#8217;re not &#8220;leveraging your React skills.&#8221; You&#8217;re learning a new platform that happens to use the same language.</p><p>For a Rails team with server-rendered views, React Native often means more work than going fully native. At least with native you&#8217;re only learning one new thing.</p><h2><strong>App Store rejections are rarely about the tech</strong></h2><p>Teams spend a lot of time worrying about whether Apple will reject their Hotwire Native app for being &#8220;just a web view.&#8221; In practice, I&#8217;ve almost never seen this happen.</p><p>Apple&#8217;s actual rejections are about business rules. You need Sign in with Apple if you offer any third-party login. You need in-app purchases if you sell digital goods. You need a way to delete accounts. You need your app to do something that justifies its existence in the stores.</p><p>I&#8217;ve had apps rejected because the onboarding flow was confusing, because the screenshots didn&#8217;t match the app, and because the privacy policy link was broken. <strong>Never because the content was HTML.</strong></p><p>The framework doesn&#8217;t matter to Apple. What matters is that your app provides value and follows their guidelines.</p><h2><strong>Small teams ship faster with less native code</strong></h2><p><strong>The fastest Hotwire Native launch I&#8217;ve been part of went from zero to the App Store in seven weeks.</strong> The team was two Rails developers who had never opened Xcode before. They shipped with zero custom native screens, a handful of bridge components, and their existing Rails views.</p><p>The slowest projects I&#8217;ve seen took 6-8 months. They were teams that tried to build native equivalents of screens they already had on the web. Not because those screens needed to be native, but because someone assumed &#8220;app&#8221; meant &#8220;native UI.&#8221;</p><p>Speed comes from constraint. Build the minimum native layer, ship, then enhance based on what users actually ask for. You can always add native screens later. You can&#8217;t get back the months you spent building them too early.</p><h2><strong>You don&#8217;t need mobile developers</strong></h2><p>This is the part that&#8217;s hardest for CTOs to believe. <strong>Your Rails team can ship a mobile app.</strong> They don&#8217;t need to learn Swift and Kotlin from scratch. They need to learn just enough to set up the Hotwire Native shell and wire up bridge components.</p><p>I wrote <a href="https://amzn.to/4qjRVq2">my book</a> specifically for Rails developers with zero mobile experience. And I&#8217;ve trained developers who had never opened Xcode to ship real apps to the App Store. The learning curve is real, but it&#8217;s weeks, not months.</p><p>The alternative is hiring iOS and Android developers, which means new team members, new processes, and ongoing coordination between three teams. For a small company, that&#8217;s often a bigger risk than the technical challenge of learning a new framework.</p><h2><strong>What this all comes down to</strong></h2><p>The right framework depends on your situation. That&#8217;s why I wrote the comparison guide, so you can look at the tradeoffs and make an informed decision. <a href="https://masilotti.com/rails-developers-guide-to-mobile-app-frameworks/">Check it out on my website.</a></p><p>And if you&#8217;d rather have someone help you figure out the right approach before writing any code, that&#8217;s exactly what the <a href="https://masilotti.com/services/mobile-playbook/">Mobile Playbook</a> is for.</p><p>I&#8217;d love to hear what framework you&#8217;re using or considering. Leave a comment and let me know.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.masilotti.com/p/what-ive-learned-from-shipping-25/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-ive-learned-from-shipping-25/comments"><span>Leave a comment</span></a></p>]]></content:encoded></item><item><title><![CDATA[AI makes it easier to build the wrong thing faster]]></title><description><![CDATA[Why I threw away 20,000+ lines of working code and started over with nothing.]]></description><link>https://newsletter.masilotti.com/p/ai-makes-it-easier-to-build-the-wrong</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/ai-makes-it-easier-to-build-the-wrong</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Tue, 10 Feb 2026 18:52:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wK-I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.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_!wK-I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wK-I!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 424w, https://substackcdn.com/image/fetch/$s_!wK-I!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 848w, https://substackcdn.com/image/fetch/$s_!wK-I!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!wK-I!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wK-I!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg" width="1200" height="798.6263736263736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:969,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:4405717,&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/186892849?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.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_!wK-I!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 424w, https://substackcdn.com/image/fetch/$s_!wK-I!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 848w, https://substackcdn.com/image/fetch/$s_!wK-I!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!wK-I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d1b146b-e8f4-460c-83fd-8df9185bbcb4_8000x5323.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 role="img" 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><title></title><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>I just spent two weeks building a custom newsletter platform from scratch. Auth, subscriptions, analytics, drip campaigns, email previews, Stripe integration&#8230; the list goes on.</p><p>I did it all with Claude. 20,000+ lines of code. And it worked!</p><p>Then I deleted it.</p><h2><strong>Content in two places felt broken</strong></h2><p>For six months I&#8217;ve had this nagging feeling that my content lives in the wrong place.</p><p>My newsletter is on Substack. My website is a Jekyll site. Every time I wrote something, I&#8217;d copy it to both places. Then I&#8217;d update one and forget to update the other. It felt messy. And all the SEO/AI juice I thought I was losing by having my stuff live on two separate domains.</p><p>So I decided to fix it. I&#8217;d build one system where everything lives together. My website would become my newsletter platform. No more duplication. No more sync issues. One source of truth.</p><p>I fired up a new Rails app and got to work.</p><h2><strong>What I built</strong></h2><p>In two weeks, I had:</p><ul><li><p>User authentication with free and paid tiers</p></li><li><p>Subscription flows through Stripe</p></li><li><p>Admin interface for managing posts</p></li><li><p>Multiple analytics dashboards</p></li><li><p>Email sending through Postmark</p></li><li><p>Drip campaigns through Bento</p></li><li><p>Dynamic CTAs based on whether you were signed in, free, or paid</p></li><li><p>A custom markdown renderer (I&#8217;m still pretty proud of this one!)</p></li></ul><p>Over 20,000 lines of code. It all worked. I set up my Render config, deployed it, and watched it run.</p><p>But then I thought: what happens if&#8230;?</p><h2><strong>Then the &#8220;what ifs&#8221; started</strong></h2><p>What happens if a background job fails and I don&#8217;t notice?</p><p>What happens if a job gets stuck in a loop and sends 500 emails to one person?</p><p>What happens if I forget some esoteric header in the emails and my domain gets flagged for spam?</p><p>What happens if I leak a credential?</p><p>What happens if Cloudflare caches a page wrong and someone sees another user&#8217;s session?</p><p>None of these were unsolvable. I could always write more code, add more monitoring, and build more safeguards.</p><p>But that&#8217;s when it hit me: I didn&#8217;t have a code problem. <strong>I had a decision problem.</strong></p><h2><strong>The real answer took an afternoon</strong></h2><p>The real question was never &#8220;how do I build a unified platform?&#8221; It was &#8220;where should my content live?&#8221;</p><p>Once I actually sat down and thought about it, the answer was obvious.</p><p>Substack handles content. That&#8217;s what it&#8217;s good at. Sending emails, managing subscribers, handling spam complaints, dealing with deliverability. All the stuff I was about to become responsible for.</p><p>My website is a business card. A list of my services, an about page, and links to everything else. Simple, static, and practically zero maintenance.</p><p>That&#8217;s it! <strong>No code required.</strong> Just a decision about what goes where.</p><p>I could have made that decision in an afternoon. Instead, I spent two weeks building something I didn&#8217;t need.</p><h2><strong>Without AI, I would have quit at 20%</strong></h2><p>If I had tried to build this myself, without AI, I probably would have quit at 20%. It would have been too much work with too many moving parts. Just&#8230; not worth it.</p><p>But Claude got me to 90%. I was cranking out features, knocking off todos and shipping tons of code. I <em>felt</em> productive.</p><div class="pullquote"><p>I was on a ship going full speed with no idea where I was headed.</p></div><p>And that&#8217;s the trap. AI removes the friction that used to make us stop and ask &#8220;is this even worth building?&#8221; You can get so far down the wrong path that you forget to check if it&#8217;s the right path.</p><h2><strong>Building is easy. Building the right thing is hard.</strong></h2><p>A few weeks ago I wrote about building something real, almost entirely with AI. My takeaway was that code isn&#8217;t the bottleneck anymore.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;dd6430cd-632f-4ddc-a8ef-10b919345e92&quot;,&quot;caption&quot;:&quot;For the past few weeks I ran an experiment: build something real, almost entirely with AI. Here&#8217;s what I learned, what&#8217;s shifting in how I think about code, and what I&#8217;m honestly terrified about.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;md&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;I went off the deep end with AI&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-26T18:13:42.264Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!PlZj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/i-went-off-the-deep-end-with-ai&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:183812601,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:9,&quot;comment_count&quot;:11,&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><p>But here&#8217;s what I didn&#8217;t fully appreciate: that cuts both ways.</p><p>If building is easy, building the <em>wrong thing</em> is just as easy. You can spin up a working prototype in a weekend. You can add features faster than you can decide if you need them.</p><p>The constraint is no longer &#8220;can we build this?&#8221; It&#8217;s &#8220;should we?&#8221;</p><p>And that&#8217;s a <em>strategy</em> question, not a <em>code</em> question.</p><p>If I had spent an afternoon thinking through what I actually needed, I would have ended up exactly where I am now. Content on Substack, everything else on my website. No code. No maintenance. No &#8220;what ifs&#8221; keeping me up at night.</p><p>Instead, I learned it the expensive way. Two weeks of work, deleted.</p><h2><strong>Strategy before code</strong></h2><p>Before you start prompting Claude, ask yourself: what problem are you actually solving?</p><p>Not &#8220;what would be cool to build?&#8221; Not &#8220;what can I build?&#8221; What do you actually <em>need</em>?</p><p>The answer might be a decision, not a feature. Maybe a conversation instead of a codebase. Or like me, an afternoon of thinking, not two weeks of &#8220;shipping&#8221;.</p><p>Don&#8217;t let the ease of building trick you into skipping the strategy. It&#8217;s more expensive than it looks.</p><p>This is a big part of why I now <a href="https://masilotti.com/services/mobile-playbook/">help teams figure out what to build</a> before they start building. The decision, not just the code.</p>]]></content:encoded></item><item><title><![CDATA[Introducing Permission Not Required]]></title><description><![CDATA[A new podcast with Colleen Schnettler about building independent businesses on your own terms in the age of AI.]]></description><link>https://newsletter.masilotti.com/p/introducing-permission-not-required</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/introducing-permission-not-required</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 04 Feb 2026 14:24:57 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/186249552/e345ddbfe537b00c1f7219c5810fa120.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Always a guest, never a host. Until now!</p><p><span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Colleen&quot;,&quot;id&quot;:1630672,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee34a742-7e90-4a07-a4c2-95974cad25c9_144x144.png&quot;,&quot;uuid&quot;:&quot;6bce44ea-ec17-4668-b961-cb8b0868b8ba&quot;}" data-component-name="MentionToDOM"></span> and I just launched a new podcast, <strong><a href="https://nopermission.fm">Permission Not Required</a></strong>.</p><p>Colleen builds developer tools and helps businesses figure out the intersection of coding and marketing. You probably know her from <a href="https://softwaresocial.dev">Software Social</a> or <a href="https://www.simplefileupload.com">Simple File Upload</a>.</p><p>And I help Rails businesses ship mobile apps. But you&#8217;re reading <a href="https://newsletter.masilotti.com">my newsletter</a>, so you probably knew that already!</p><p>We&#8217;ve been chatting about our businesses every week for years. Literally. We talk strategy, product ideas, client drama, wins, losses&#8230; all of it.</p><p>Those calls have been so valuable that we figured it was time to share them with everyone. Plus, I&#8217;ve always wanted to host a podcast. &#128517;</p><p>So here it is. Real conversations about building independent businesses without asking for permission. And trying to stay relevant in the age of AI.</p><p>Subscribe on your favorite podcast player:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://www.youtube.com/playlist?list=PLRspdYCVVoo5BCC7Vj1yfF69Ku-qHR8J7" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cO9P!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 424w, https://substackcdn.com/image/fetch/$s_!cO9P!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 848w, https://substackcdn.com/image/fetch/$s_!cO9P!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 1272w, https://substackcdn.com/image/fetch/$s_!cO9P!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cO9P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png" width="321" height="81" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa5e1c5c-611d-4b92-8195-187664490b30_321x81.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:81,&quot;width&quot;:321,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3356,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://www.youtube.com/playlist?list=PLRspdYCVVoo5BCC7Vj1yfF69Ku-qHR8J7&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/186249552?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.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_!cO9P!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 424w, https://substackcdn.com/image/fetch/$s_!cO9P!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 848w, https://substackcdn.com/image/fetch/$s_!cO9P!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 1272w, https://substackcdn.com/image/fetch/$s_!cO9P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa5e1c5c-611d-4b92-8195-187664490b30_321x81.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://podcasts.apple.com/podcast/id1872495512" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xh9s!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 424w, https://substackcdn.com/image/fetch/$s_!xh9s!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 848w, https://substackcdn.com/image/fetch/$s_!xh9s!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 1272w, https://substackcdn.com/image/fetch/$s_!xh9s!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xh9s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png" width="320" height="80" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:80,&quot;width&quot;:320,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13905,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://podcasts.apple.com/podcast/id1872495512&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/186249552?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.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_!xh9s!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 424w, https://substackcdn.com/image/fetch/$s_!xh9s!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 848w, https://substackcdn.com/image/fetch/$s_!xh9s!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 1272w, https://substackcdn.com/image/fetch/$s_!xh9s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6806a04-c5ff-4cea-aaf5-036000c072b9_320x80.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://open.spotify.com/show/7qaCaikR046PBjUh9JfAJl" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XKnb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 424w, https://substackcdn.com/image/fetch/$s_!XKnb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 848w, https://substackcdn.com/image/fetch/$s_!XKnb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 1272w, https://substackcdn.com/image/fetch/$s_!XKnb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XKnb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png" width="320" height="80" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/956f2063-a352-435d-9efe-a008e33efc24_320x80.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:80,&quot;width&quot;:320,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9508,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://open.spotify.com/show/7qaCaikR046PBjUh9JfAJl&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/186249552?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.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_!XKnb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 424w, https://substackcdn.com/image/fetch/$s_!XKnb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 848w, https://substackcdn.com/image/fetch/$s_!XKnb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 1272w, https://substackcdn.com/image/fetch/$s_!XKnb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F956f2063-a352-435d-9efe-a008e33efc24_320x80.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[I went off the deep end with AI]]></title><description><![CDATA[What happens when code becomes the easy part? Excitement, dread, and what this means for my solo-run business.]]></description><link>https://newsletter.masilotti.com/p/i-went-off-the-deep-end-with-ai</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/i-went-off-the-deep-end-with-ai</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Mon, 26 Jan 2026 18:13:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PlZj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png" 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_!PlZj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PlZj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 424w, https://substackcdn.com/image/fetch/$s_!PlZj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 848w, https://substackcdn.com/image/fetch/$s_!PlZj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 1272w, https://substackcdn.com/image/fetch/$s_!PlZj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PlZj!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png" width="1200" height="629.6703296703297" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:764,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:5154746,&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/183812601?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png&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_!PlZj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 424w, https://substackcdn.com/image/fetch/$s_!PlZj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 848w, https://substackcdn.com/image/fetch/$s_!PlZj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.png 1272w, https://substackcdn.com/image/fetch/$s_!PlZj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28c2ca74-dddf-4909-843c-25211b5d700d_2400x1260.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 role="img" 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><title></title><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>For the past few weeks I ran an experiment: build something real, almost entirely with AI. Here&#8217;s what I learned, what&#8217;s shifting in how I think about code, and what I&#8217;m honestly terrified about.</p><p><em>I&#8217;m Joe Masilotti, author of <a href="https://amzn.to/499Vf0G">Hotwire Native for Rails Developers</a>. This newsletter is where I share weekly thoughts on Hotwire Native, indie consulting, and doing great work while staying present at home.</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><h2><strong>Building a real product with AI</strong></h2><p>I just launched <a href="https://purchasekit.com/">PurchaseKit</a>, my take on in-app purchases for iOS and Android apps. And I built more than 90% of it with AI.</p><p>This isn&#8217;t a trivial product. It has <a href="https://github.com/orgs/PurchaseKit/repositories">multiple moving parts</a>: an iOS package, an Android package, a Ruby gem, and a hosted SaaS that coordinates between all the pieces. I built out the original iOS and Android proof-of-concepts, then let AI rip on pretty much everything else. I took a very iterative approach, having it build small features one at a time. Not too different from how I would build it by hand.</p><p>And to my shock (and horror) it did so with amazing ease.</p><p>I had to hold its hand a bunch, sure. There were a few hallucinations here and there, mostly related to Hotwire Native (which makes sense since there probably isn&#8217;t a lot of content on that out there, <a href="https://newsletter.masilotti.com/">besides from yours truly</a>!). But the Rails stuff? The boring CRUD of managing users, teams, billing, even webhooks&#8230; it churned those out without much editing or even review on my end. It was pretty darn close to perfect.</p><p>There have been bugs, sure. I only launched this earlier this month. But the fixes aren&#8217;t too far from copying a Honeybadger stack trace and dropping it into Claude Code. I can have a fix done in a few minutes without ever needing to know the context (which maybe isn&#8217;t actually a good thing).</p><p>I make sure a regression test is created first, then the AI can verify the fix by making sure that test passes. I like this approach because I can practically guarantee the issue will be fixed, instead of false positive test runs.</p><h2><strong>Code is no longer the hard part</strong></h2><p>Folks are always saying that code was never the hard part. And, as a developer, I always dismissed that. &#8220;They don&#8217;t know what they&#8217;re talking about, of course code is hard!&#8221;</p><p>Well, I think I&#8217;ve flipped on that.</p><p>Writing code is no longer the bottleneck. Coming up with <em>what</em> to code and how to get it into the right people&#8217;s hands is <em>relatively</em> harder than ever.</p><p><strong>My 2026 mindset is to value code a little less.</strong> Like giving more stuff away for free. The iOS, Android, and gem for PurchaseKit are all MIT licensed, even though I originally planned a custom license that wouldn&#8217;t allow you to use it outside of a PurchaseKit integration.</p><p>Code is cheap now. Knowing <em>what</em> to build, how to piece it all together, where to host it reliably, how to provide support when things eventually break&#8230; all the <em>business</em> stuff is the hard part. It always has been, but code just took up so much of my time that it was hard to realize that.</p><h2><strong>What this means for my consulting business</strong></h2><p>For the past 10+ years I&#8217;ve helped businesses bring their Rails apps to iOS and Android with Hotwire Native. I&#8217;ve shipped 25+ apps to the App Store and Google Play this way.</p><p>So what does this mean for a consultant like myself? Where I &#8220;sell&#8221; code to businesses for money?</p><p>Honestly, I&#8217;m not quite sure.</p><p>I&#8217;m still going to offer <a href="https://masilotti.com/services/">&#8220;Zero to App Store&#8221; services</a>. I think there are lots of businesses out there that don&#8217;t want to do this on their own, even with the help of AI. They value someone with a track record. They know what they get will be top quality, will work, will be reliable&#8230; and most importantly, if something goes wrong they know exactly who to call: <em>me</em>.</p><p><strong>I&#8217;d be lying to myself if I said that this will continue to be 100% of my consulting business forever.</strong></p><p>I&#8217;m going to need to shift to something that vibes better with agent-first coding. Am I offering a &#8220;Joe AI&#8221; that has all of my 10+ years of experience distilled into a GPT? Or a starter kit that is so well documented that AI can&#8217;t possibly mess something up? Or just offering more advisory contracts where teams continue to build and I watch over their shoulder, review PRs, and make sure they don&#8217;t shoot themselves in the foot?</p><p>I&#8217;m not sure yet.</p><div class="pullquote"><p><strong>But I&#8217;m excited, and a bit terrified, to experiment a bunch this year.</strong></p></div><h2><strong>What happens when I&#8217;m not typing</strong></h2><p>Here&#8217;s something I didn&#8217;t expect: I&#8217;m slowly becoming re-addicted to social media.</p><p>Every time Claude does work I have the urge, and usually give in, to checking on something. Sometimes I fool myself and pretend that checking email for the 100th time is &#8220;productive&#8221;. But who am I kidding? And don&#8217;t get me started on how often I&#8217;ve refreshed Twitter&#8230;</p><p>I need to figure out something to do in the &#8220;down time&#8221; when an agent works. I&#8217;ve been experimenting with kicking off a new agent in another terminal tab. But I find that I lose context too quickly if the projects aren&#8217;t closely related. I also absolutely <em>cannot</em> write anything of substance or value in 30 second increments so that&#8217;s off the table.</p><p>Maybe I&#8217;ll just bring a book to my coworking space&#8230; and be that psycho who reads with his laptop open right behind the pages.</p><p>I&#8217;m not sure if this is how I&#8217;ll work forever. This was an experiment, and I&#8217;m still processing what it means.</p><div><hr></div><p>Are you feeling this weird mix of excitement and dread too? Leave a comment, I&#8217;d love to hear what you&#8217;re grappling with.</p><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">Or follow along as I figure this out.</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>And if you want to try AI-assisted coding yourself, <a href="https://claude.ai/referral/RMR8WnBfsw">here are a few free Claude Code invites</a>.</p>]]></content:encoded></item><item><title><![CDATA[Hotwire Native: In-app Purchases on iOS]]></title><description><![CDATA[The full flow from HTML paywall to real-time success message, and how a single UUID is all it takes to tie it together.]]></description><link>https://newsletter.masilotti.com/p/hotwire-native-deep-dive-in-app-purchases</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/hotwire-native-deep-dive-in-app-purchases</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 22 Jan 2026 16:05:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/44cc202a-e937-4e01-b8e4-afff9110b58c_2400x1260.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>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.</p><p><em>Side note: If you&#8217;re selling to US or EU customers, Apple now allows linking to external payment providers like Stripe. But they show a scary warning screen that tanks conversion. For most apps, native IAPs are still the better path.</em></p><p>This article walks through the full flow: triggering a purchase from your HTML paywall, handling it in Swift, processing Apple&#8217;s webhooks, and updating the UI in real-time with Turbo Streams. I&#8217;ve built this for a bunch of clients over the past 10+ years, and the approach here is exactly how <a href="https://purchasekit.dev/">PurchaseKit</a> works under the hood.</p><h2><strong>The in-app purchase flow</strong></h2><p>Here&#8217;s what we&#8217;re building:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zpGI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zpGI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 424w, https://substackcdn.com/image/fetch/$s_!zpGI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 848w, https://substackcdn.com/image/fetch/$s_!zpGI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 1272w, https://substackcdn.com/image/fetch/$s_!zpGI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zpGI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png" width="728" height="448.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:897,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:250243,&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/183849101?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zpGI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 424w, https://substackcdn.com/image/fetch/$s_!zpGI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 848w, https://substackcdn.com/image/fetch/$s_!zpGI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.png 1272w, https://substackcdn.com/image/fetch/$s_!zpGI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6613148f-69db-4348-b4ba-635b3fe4b79a_2133x1314.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 role="img" 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><title></title><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><blockquote><p><em>A heads up that these code examples are abbreviated to fit. Reference the downloadable source code below for the full implementation.</em></p></blockquote><h3><strong>1. Trigger the purchase from HTML</strong></h3><p>A button fires a Stimulus controller, passing the product ID and a token to identify the user from the HTML.</p><pre><code>&lt;div data-controller="bridge--paywall"&gt;
  &lt;button
      data-action="bridge--paywall#subscribe"
      data-product-id="com.example.pro.annual"
      data-user-id="&lt;%= Current.user.id %&gt;"&gt;
    Subscribe for $4.99 per month
  &lt;/button&gt;
&lt;/div&gt;</code></pre><h3><strong>2. Send the request to native</strong></h3><p>The bridge controller extracts the data and sends it to the iOS bridge component.</p><pre><code>export default class extends BridgeComponent {
  static component = "paywall"
 &#8203;
  subscribe(event) {
    const productId = event.currentTarget.dataset.productId
    const userId = event.currentTarget.dataset.userId
    this.send(&#8221;purchase&#8221;, { productId, userId })
  }
}</code></pre><h3><strong>3. Call StoreKit</strong></h3><p>iOS receives the message, fetches the product from StoreKit, and initiates the purchase with the user&#8217;s identifier.</p><pre><code>class PaywallComponent: BridgeComponent {
    override class var name: String { "paywall" }
&#8203;
    override func onReceive(message: Message) {
        let id = message.data["productId"]
        let userId = message.data["userId"]
&#8203;
        let product = Product.products(for: [id]).first!
        product.purchase(options: [.appAccountToken(userId)])
    }
}</code></pre><h3><strong>4. Handle Apple&#8217;s webhook</strong></h3><p>After a successful purchase, Apple POSTs a signed JSON Web Signature (JWS) to your Rails server. This is decoded to find the user via the identifier and their subscription is updated.</p><pre><code>class AppStoreWebhooksController &lt; ApplicationController
  def create
    payload = decode(params["signedPayload"])
    info = decode(payload["data"]["signedTransactionInfo"])
    user = User.find(info["appAccountToken"])
 &#8203;
    case payload["notificationType"]
    when "SUBSCRIBED"
      user.subscription.update!(
        product_id: info["productId"],
        expires_at: Time.at(info[&#8221;expiresDate&#8221;] / 1000),
        status: :active
      )
    end
 &#8203;
    head :ok
  end
end</code></pre><h3><strong>5. Broadcast the update</strong></h3><p>When the subscription saves, a Turbo Stream is broadcasted to replace the paywall with a success message.</p><pre><code>class Subscription &lt; ApplicationRecord
  belongs_to :user
 &#8203;
  after_commit :broadcast_update, if: :active?
 &#8203;
  def broadcast_update
    Turbo::StreamsChannel.broadcast_replace_to(
      user, :paywall,
      target: "paywall",
      partial: "paywalls/success"
    )
  end
end</code></pre><p>The key insight is the <code>appAccountToken</code>. When iOS makes a purchase, it can pass a UUID that Apple will echo back in every webhook related to that transaction. We use this to correlate purchases to users: embed the user&#8217;s ID in the token, and when Apple&#8217;s webhook arrives, extract it to know who just subscribed. </p><p>Which means there&#8217;s no session cookies, callback URLs, or &#8220;pending purchase&#8221; records to clean up.</p><p>The <strong>product ID</strong> (like <code>com.yourapp.pro.monthly</code>) is how you identify what&#8217;s being purchased. You&#8217;ll hardcode these in your HTML buttons and receive them back in webhooks.</p><p>This approach has a bunch of benefits.</p><ul><li><p><strong>You can design and iterate on your paywall in Rails.</strong> And that means you can run pricing experiments or complete redesigns without having to submit new binaries to the App Store.</p></li><li><p><strong>Subscription data lives in your Rails database, not on Apple&#8217;s servers.</strong> That means you can use your usual <code>user.subscribed?</code> instead of having to do a round-trip to the device.</p></li><li><p>Listening for webhooks ensures that <strong>your subscription data is always kept in sync</strong>, even if the user never opens the app again. This is super helpful when someone subscribes in the app - you&#8217;ll want to give them access on web, too.</p></li></ul><p>If this looks like a lot of moving pieces, well&#8230; you&#8217;re right. <a href="https://purchasekit.dev/">PurchaseKit</a> handles all of this (plus Android, error handling, and edge cases) so you can skip the complexity. But if you want to understand how it works under the hood, or build it yourself, read on.</p><p>Paid subscribers get the complete implementation: every line of Swift, JavaScript, and Ruby explained step-by-step, plus a downloadable demo app with working StoreKit code you can run on your device today.</p>
      <p>
          <a href="https://newsletter.masilotti.com/p/hotwire-native-deep-dive-in-app-purchases">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Office Hours Recap - January]]></title><description><![CDATA[Tab navigation, audio playback, Liquid Glass modal fixes, and more&#8230;]]></description><link>https://newsletter.masilotti.com/p/office-hours-recap-january</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/office-hours-recap-january</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 15 Jan 2026 16:42:27 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a960368a-e3f6-4427-96bd-64c7b49cb1f8_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends, this month&#8217;s office hours session was packed! We had a great mix of technical deep dives, real-world app stories, and some good discussion around PurchaseKit.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;a826ef82-bbac-478b-a32b-0aad5d40b0d1&quot;,&quot;caption&quot;:&quot;Every month I host an hour-long Zoom session for Hotwire Native developers to ask questions, share progress, and connect with others building the same way.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Hotwire Native Office Hours&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;Hotwire Native consultant and author obsessed with building mobile apps powered by Ruby on Rails. Here to prove you can run a thriving independent business without sacrificing family time.&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-09-17T00:24:39.338Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!Z9wc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1475c5dd-6c17-4d31-a80f-cf975c7cfcc7_4032x2565.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/office-hours&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:173808983,&quot;type&quot;:&quot;page&quot;,&quot;reaction_count&quot;:2,&quot;comment_count&quot;:0,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Hotwire Native Weekly&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;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>Who showed up</h2><ul><li><p><strong>A successful founder</strong> shared something wild: They shipped 80+ iOS apps and 90+ Android apps, all powered by a single Rails backend. The apps are for different organizations using their platform, each with their own branding. A great example of Hotwire Native&#8217;s power for white-label solutions.</p></li><li><p><strong>A Rails developer</strong> demoed their social app and asked about some UI quirks after upgrading to Liquid Glass.</p></li><li><p><strong>Someone building a note-taking app</strong> was looking for advice on icon creation and asset management.</p></li><li><p><strong>A developer using Claude</strong> to generate Rails code for their Hotwire Native app sparked a discussion about where AI shines (CRUD, boilerplate) and where it still needs human oversight (Hotwire Native integrations).</p></li></ul><h2>What we covered</h2><p>Here&#8217;s what we covered in the hour-long, private Zoom session.</p><h3>Flashing screens during navigation</h3><p>One question was about a flash occurring when navigating between pages. The app has a web-based toolbar shown on every screen and when it disappears then reappears, it is jarring.</p><p>Unfortunately, this is a limitation of how view controllers and fragments work on iOS and Android. When Hotwire Native navigates, it covers the whole web view and shows a spinner while loading.</p><p>I offered two ways to solve this:</p><ol><li><p>Move your toolbar to native so it stays persistent during navigation</p></li><li><p>Use Turbo Frames to update parts of the page without triggering full Hotwire Native routing (essentially creating a single-page app experience for that section)</p></li></ol><h3>Switching tabs from a link</h3><p>Another developer wanted links to automatically switch the tab bar. For example, clicking &#8220;See all Posts&#8221; from the Home tab should switch to the <em>Posts</em> tab.</p><p>You need to build this yourself using path configuration. Add a property like <code>tab_identifier</code> to your path configuration rules. Then in your <code>NavigatorDelegate</code> handle&#8217;s function, check for that property, find the matching tab, and switch to it before accepting the route.</p><pre><code>if let tabIdentifier = properties[&#8221;tab_identifier&#8221;] as? String {
    // Find and switch to the correct tab
    tabBarController.selectedIndex = tabIndex(for: tabIdentifier)
}</code></pre><h3>Stopping audio when leaving a page</h3><p>We also discussed stopping audio playback when navigating away from a page.</p><p>A few options here:</p><ol><li><p>Subclass the Hotwire Native view controller and override <code>viewWillDisappear</code></p></li><li><p>Use a Stimulus controller that listens for disconnect and pauses the audio</p></li><li><p>Use a native audio player instead (bonus: you get background playback and lock screen controls)</p></li></ol><p>The native player approach is often the best solution if you are playing more than simple sound effects. But the Stimulus controller offers the least native code, it should be deployable with only changes to the Rails codebase.</p><h3>Icon creation and app assets</h3><p>A developer asked what I use for icons. Here&#8217;s my go-to stack:</p><ul><li><p><a href="https://developer.apple.com/sf-symbols/">SF Symbols</a> for iOS-only apps (almost 7,000 symbols to choose from)</p></li><li><p><a href="https://fonts.google.com/icons">Material Symbols</a> for Android or cross-platform (download SVGs)</p></li><li><p><a href="https://icons.getbootstrap.com">Bootstrap Icons</a> or <a href="https://heroicons.com">Heroicons</a> for web-focused icon sets</p></li><li><p><a href="https://thenounproject.com">The Noun Project</a> for app icons (~$5 per icon, varying quality)</p></li></ul><p>The Noun Project is my sleeper pick and is perfect for something slightly unique without breaking the bank by hiring a designer for a fully custom design.</p><h3>Modal buttons with Liquid Glass</h3><p>Someone noticed users were tapping the checkmark in the top-right corner of modals instead of his custom Post button. This started happening after the Liquid Glass update.</p><p>This is occurring because <code>Hotwire.config.showDoneButtonsOnModals = true</code> will automatically add the &#8220;done&#8221; button to the upper right of every modal. But on iOS 26+, this becomes a check mark!</p><p>The best solution here is to implement a <code>FormComponent</code> that adds a native button that disables while the form is submitting.</p><h3>PurchaseKit questions</h3><p>Someone asked about <a href="https://purchasekit.dev">PurchaseKit</a>, which I launched earlier this month.</p><p>The short version: iOS and Android packages plus a Ruby gem that handle in-app purchases. You write your paywall in ERB, the native packages handle StoreKit and Play Billing, and webhooks keep your Rails database in sync.</p><p>It is free for up to 10 paying subscribers, then $99/month. If you are interested, check it out! Oh, and <strong>if you create an account in January, I&#8217;ll personally onboard you.</strong></p><h2>What&#8217;s next?</h2><p>Next month&#8217;s office hours is on <a href="https://newsletter.masilotti.com/p/office-hours">Thursday, February 5</a>. Paid subscribers get access to the live call and can ask questions in real time.</p><p>If you have a topic you would like me to cover, leave a comment or reply to this email!</p>]]></content:encoded></item><item><title><![CDATA[I finally built the in-app purchase tool I wish I had years ago]]></title><description><![CDATA[Native payment sheets on iOS and Android. Subscription data in your Rails database.]]></description><link>https://newsletter.masilotti.com/p/i-finally-built-the-in-app-purchase</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/i-finally-built-the-in-app-purchase</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Thu, 08 Jan 2026 16:02:53 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e24fcf4b-f33c-49fe-be1d-0efe38c5cbcb_2400x1260.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends, I&#8217;m finally shipping something I&#8217;ve wanted to build for <em>years</em>.</p><p><strong>Say hi to <a href="https://purchasekit.dev/">PurchaseKit</a>: in-app purchase infrastructure for Hotwire Native apps.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a4Hv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a4Hv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 424w, https://substackcdn.com/image/fetch/$s_!a4Hv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 848w, https://substackcdn.com/image/fetch/$s_!a4Hv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 1272w, https://substackcdn.com/image/fetch/$s_!a4Hv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a4Hv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif" width="1280" height="831" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:831,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:915230,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/183709769?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!a4Hv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 424w, https://substackcdn.com/image/fetch/$s_!a4Hv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 848w, https://substackcdn.com/image/fetch/$s_!a4Hv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 1272w, https://substackcdn.com/image/fetch/$s_!a4Hv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbca289f5-6484-48cb-b5a6-af7fa3a154ae_1280x831.gif 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 role="img" 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><title></title><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>You can now add subscriptions to your Hotwire Native app without writing <em>any</em> native code or worrying about keeping your database in sync with Apple and Google.</p><p>I&#8217;ve been adding some version of this to client apps for years. And every single time, it&#8217;s a huge pain for me <strong>and my clients</strong>. StoreKit on iOS. Play Billing on Android. Webhook verification. Receipt validation. Subscription lifecycles&#8230;</p><p>And the worst part? Keeping your Rails database in sync with what Apple and Google think the subscription status is.</p><p>I&#8217;ve talked to so many developers who don&#8217;t go native <em>specifically</em> because they don&#8217;t want to deal with this stuff. Apple and Google require in-app purchases for certain apps, and the complexity just isn&#8217;t worth it.</p><p>So I finally built something generic that everyone can use. I&#8217;ve launched over 25 Hotwire Native apps. This is the billing infrastructure I use.</p><h2><strong>How it works</strong></h2><p><strong>Native packages for iOS and Android (bridge components) handle the purchase flow.</strong> StoreKit 2 on iOS, Play Billing on Android. Your Rails app never touches the native payment sheets directly. And you never touch the native code.</p><p><strong>You write your paywall in ERB.</strong> Style it like the rest of your app. And because it&#8217;s server-rendered, you can deploy pricing experiments without pushing new binaries to the app stores.</p><pre><code>&lt;%= purchasekit_paywall customer_id: current_user.id do |paywall| %&gt;
  &lt;%= paywall.plan_option product: @annual, selected: true do %&gt;
    Annual - &lt;%= paywall.price %&gt;/year
  &lt;% end %&gt;
&#8203;
  &lt;%= paywall.plan_option product: @monthly do %&gt;
    Monthly - &lt;%= paywall.price %&gt;/month
  &lt;% end %&gt;
&#8203;
  &lt;%= paywall.submit &#8220;Subscribe&#8221; %&gt;
&lt;% end %&gt;</code></pre><p><strong>Webhooks are normalized and forwarded to your Rails app.</strong> Apple and Google send completely different webhook formats. PurchaseKit normalizes them into one consistent structure before forwarding to your server.</p><p><strong>Your subscription data lives in your database.</strong> Not on Apple&#8217;s servers. Not on Google&#8217;s servers. In <em>your</em> database, right next to everything else.</p><p>PurchaseKit keeps everything in sync via webhooks. When a subscription renews, cancels, or expires, your database updates automatically. If you&#8217;re using Pay, it creates and updates <code>Pay::Subscription</code> records. If not, use event callbacks to handle it however you want.</p><pre><code>PurchaseKit.configure do |config|
  config.on(:subscription_created) do |event|
    user = User.find(event.customer_id)
    user.subscriptions.create!(...)
  end
end</code></pre><p>Your <code>current_user.subscribed?</code> checks just work. No API calls. No stale data.</p><h2><strong>Pricing</strong></h2><p>I&#8217;m launching PurchaseKit with a free tier so you can try it out and validate your idea before paying anything.</p><p><strong>Free</strong> - Up to 10 paying customers for 1 app. No credit card required. This is enough to build your paywall, test the full flow in sandbox, and launch to real, paying users.</p><p><strong>Pro ($99/mo)</strong> - Unlimited paying customers for up to 3 apps. For most indie developers and small teams, this is the plan.</p><p><strong>Business ($399/mo)</strong> - Unlimited everything, plus concierge onboarding where I personally help you get set up.</p><h3><strong>January launch offer</strong></h3><p>I want to make sure early adopters have a great experience. So here&#8217;s the deal:</p><p><strong>If you create an account in January, I&#8217;ll personally onboard you.</strong> We&#8217;ll do a video call to set up your iOS, Android, and Rails code. We&#8217;ll walk through App Store Connect and Google Play integration. You&#8217;ll walk away with a working paywall in your app.</p><p>This is normally a Business tier perk ($399/mo). For January, it&#8217;s free. Even on the Free plan. Just reply to this email to get on my calendar.</p><p>Next week I&#8217;m publishing a deep dive on in-app purchases: all the moving pieces, the gotchas, and why this stuff is so tricky. It&#8217;ll give you a much better sense of what PurchaseKit handles under the hood.</p><p>For now, check out <a href="https://purchasekit.dev/">PurchaseKit</a> and let me know what you think.</p><p>Questions? Just reply to this email. I can&#8217;t wait to see what you build!</p>]]></content:encoded></item><item><title><![CDATA[Hotwire Native office hours this Thursday]]></title><description><![CDATA[Paid members only, Zoom link below.]]></description><link>https://newsletter.masilotti.com/p/hotwire-native-office-hours-this-48f</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/hotwire-native-office-hours-this-48f</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Tue, 06 Jan 2026 17:47:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/195dbef9-0e06-4164-9314-458a79f36390_1200x630.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey folks,</p><p>Join me this Thursday, January 8 at 10am PT, for Hotwire Native office hours.</p><p><strong>These sessions are only available to paid newsletter subscribers.</strong></p><p>Bring your questions, share what you&#8217;re working on, or just drop by to say hi.</p><p>They&#8217;re a great way to connect with other developers building with Hotwire Native.</p><p>Last month I held office hours at a special time for our friends in Australia, and it ended up being one of the most fun, wide-ranging sessions we&#8217;ve had all year. We dug into everything from Bluetooth sleep trackers to tab reloading strategies to whether going &#8220;fully native&#8221; is actually worth it.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;d49ec753-7004-446a-af42-7247cd4bddb0&quot;,&quot;caption&quot;:&quot;I held Hotwire Native Office Hours last week at a special time for our friends in Australia, and it ended up being one of the most fun, wide-ranging sessions we&#8217;ve had all year. We dug into everything from Bluetooth sleep trackers to tab reloading strategies to whether going &#8220;fully native&#8221; is actually worth it.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Office Hours Recap - December&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;Hotwire Native consultant and author obsessed with building mobile apps powered by Ruby on Rails. Here to prove you can run a thriving independent business without sacrificing family time.&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-12-22T20:19:20.449Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!PPJp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/office-hours-recap-december&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:181066561,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:2,&quot;comment_count&quot;:0,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Hotwire Native Weekly&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;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>I&#8217;d love to see you there! Zoom and calendar links below for paid subscribers.</p>
      <p>
          <a href="https://newsletter.masilotti.com/p/hotwire-native-office-hours-this-48f">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[2025: The year consulting wasn't the only thing]]></title><description><![CDATA[For the first time, I built things that can stand on their own.]]></description><link>https://newsletter.masilotti.com/p/2025-the-year-consulting-wasnt-the</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/2025-the-year-consulting-wasnt-the</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Wed, 31 Dec 2025 14:15:31 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a5be3fe7-4773-48ed-91fd-c7f09b048aef_8640x5760.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey friends,</p><p>Sometime this fall, I noticed something I hadn&#8217;t planned: money was coming in that had nothing to do with my calendar.</p><p>Not a lot. <a href="https://masilotti.com/services/">Consulting</a> still pays the bills. But for the first time in my career, it wasn&#8217;t the <em>only</em> thing. A book sale here. A library purchase there. A few paid newsletter subscriptions trickling in.</p><p>None of it was life-changing on its own. But together? It felt like proof that maybe, just maybe, I could build things that work without me. Here&#8217;s what each one taught me.</p><h2>Creating is easy. Polishing is the real work.</h2><p><a href="https://amzn.to/3NoHyCr">My book</a> taught me that creating is easy but <em>polishing</em> is the real work.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://amzn.to/3NoHyCr" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DfmI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!DfmI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!DfmI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!DfmI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DfmI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg" width="342" height="410.4" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1500,&quot;width&quot;:1250,&quot;resizeWidth&quot;:342,&quot;bytes&quot;:95596,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:&quot;https://amzn.to/3NoHyCr&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/183054703?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.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_!DfmI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!DfmI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!DfmI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!DfmI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F314b22cc-d627-4725-9e77-ff2d806c919e_1250x1500.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 role="img" 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><title></title><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>Writing came naturally. I could bang out chapters without much friction. But editing? Revising? Working with my editor to turn rough ideas into something actually publishable? That was brutal.</p><p>I&#8217;m so grateful to the team at Pragmatic Programmers for pushing me through it. But wow, the gap between a draft and a finished book is <em>enormous</em>.</p><h2><strong>People will pay for solutions, not just my time.</strong></h2><p>My <a href="https://masilotti.com/bridge-components/">Bridge Component library</a> taught me that people will pay me for <em>solutions</em>, not just my time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://masilotti.com/bridge-components/" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s0D8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 424w, https://substackcdn.com/image/fetch/$s_!s0D8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 848w, https://substackcdn.com/image/fetch/$s_!s0D8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 1272w, https://substackcdn.com/image/fetch/$s_!s0D8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s0D8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png" width="1456" height="954" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:954,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:986318,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://masilotti.com/bridge-components/&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.masilotti.com/i/183054703?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.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_!s0D8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 424w, https://substackcdn.com/image/fetch/$s_!s0D8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 848w, https://substackcdn.com/image/fetch/$s_!s0D8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.png 1272w, https://substackcdn.com/image/fetch/$s_!s0D8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b567204-e29f-4286-ab38-b5b44559bc98_2402x1574.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 role="img" 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><title></title><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>This was my first paid product for developers, and people are actually buying it! What I love most: it&#8217;s a one-time purchase, not a subscription. And so many of the ideas come from client projects. There&#8217;s something deeply satisfying about packaging what I learn into something the whole community can use.</p><h2>My words have value beyond code.</h2><p>My <a href="https://newsletter.masilotti.com">paid newsletter</a> taught me my words have value beyond code.</p><p>This one surprised me the most. I knew people would pay for <a href="https://masilotti.com/services/">my consulting</a>. But paying for my <em>writing</em>? For my perspective on building apps, running a business, being a parent while doing all of it? That still feels a little surreal. And it&#8217;s pushing me to explore more business, parenting, and solopreneur content in 2026.</p><h2>Visibility creates its own momentum</h2><p>My <a href="https://youtu.be/VbMt_4STWIo?si=eBg3m9HjUMEH2Uko">keynote at Rails World</a> taught me that visibility creates its own momentum.</p><div id="youtube2-VbMt_4STWIo" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;VbMt_4STWIo&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/VbMt_4STWIo?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p><em>Mind-blowing</em> is the only word for it. Giving a keynote at Rails World opened doors I didn&#8217;t know existed. Huge thanks to Amanda and the Rails Foundation for the opportunity. And honestly? I kinda love being on stage. But it&#8217;s a <em>lot </em>of work.</p><h2>Looking back</h2><p>A book people buy. A library people purchase. A newsletter people subscribe to. A keynote that lives on YouTube. <em>What??</em></p><p>I didn&#8217;t plan any of it. I just kept saying yes to things that scared me.</p><p>And now, finally, I have the foundation of a business that can bring in revenue while I&#8217;m playing LEGO with the kids.</p><h2>What about you?</h2><p>What did you say yes to this year that scared you? I&#8217;d love to hear about it, leave a comment and let me know.</p><p>Here&#8217;s to more showing up in 2026! &#129395;</p>]]></content:encoded></item><item><title><![CDATA[Office Hours Recap - December]]></title><description><![CDATA[From sleep-tracking hardware to SwiftUI rewrites, here&#8217;s what came up in December&#8217;s session.]]></description><link>https://newsletter.masilotti.com/p/office-hours-recap-december</link><guid isPermaLink="false">https://newsletter.masilotti.com/p/office-hours-recap-december</guid><dc:creator><![CDATA[Joe Masilotti]]></dc:creator><pubDate>Mon, 22 Dec 2025 20:19:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PPJp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I held Hotwire Native Office Hours last week at a special time for our friends in Australia,  and it ended up being one of the most fun, wide-ranging sessions we&#8217;ve had all year. We dug into everything from Bluetooth sleep trackers to tab reloading strategies to whether going &#8220;fully native&#8221; is actually worth it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PPJp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PPJp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 424w, https://substackcdn.com/image/fetch/$s_!PPJp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 848w, https://substackcdn.com/image/fetch/$s_!PPJp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 1272w, https://substackcdn.com/image/fetch/$s_!PPJp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PPJp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99203688-ad7e-4572-acb5-21005661f347_2400x1350.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2334209,&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/181066561?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.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_!PPJp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 424w, https://substackcdn.com/image/fetch/$s_!PPJp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 848w, https://substackcdn.com/image/fetch/$s_!PPJp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.png 1272w, https://substackcdn.com/image/fetch/$s_!PPJp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99203688-ad7e-4572-acb5-21005661f347_2400x1350.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 role="img" 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><title></title><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>Here&#8217;s a recap with the highlights and the lessons you can apply to your own apps.</p><h3><strong>A real-world Bluetooth integration with Hotwire Native</strong></h3><p><a href="https://x.com/adampallozzi">Adam Pallozzi</a> kicked things off with a deep dive into a feature from <a href="https://www.sleephq.com">SleepHQ</a>: a pulse-oximeter ring that streams data through the phone overnight. It&#8217;s one of the most complex Hotwire Native integrations I&#8217;ve seen, a native Bluetooth service working alongside a Rails UI.</p><p>But there&#8217;s one issue Adam hasn&#8217;t been able to figure out just yet, iOS keeps killing the process during long background sessions, breaking the Bluetooth connection.</p><p>We talked through a few possible approaches:</p><ul><li><p>Enabling the &#8220;Acts as a Bluetooth LE accessory&#8221; background mode</p></li><li><p>Using background processing or silent push notifications to keep the app alive</p></li><li><p>&#8220;Playing audio&#8221; to keep the web view from sleeping</p></li></ul><p>This last one is interesting. Have you ever locked your phone to see audio playing on your Lock Screen? Even though you weren&#8217;t actually listening to anything?</p><p>Turns out, this is a <s>trick</s> hack to keep the web view alive! If done correctly, with all the right background modes, continuously playing audio can sometimes leave the web view alive indefinitely. Of course, it isn&#8217;t documented, and definitely not recommended, but it is an interesting idea for this scenario.</p><p>It was a great reminder that Hotwire Native doesn&#8217;t magically eliminate all native challenges. But it gives you flexibility to mix native services with a mostly Rails-driven UI. All five steps of the Bluetooth connection processes were rendered with HTML!</p><h3><strong>The tab refresh problem after authentication</strong></h3><p>Shawn asked one of the classic Hotwire Native questions: What&#8217;s the best way to refresh tabs after someone logs in or signs up?</p><p>Right now we have a few options:</p><ol><li><p>Call <code>load()</code> again on the <code>HotwireTabBarController</code></p></li><li><p>Ideally, iterate through each <code>Navigator</code> and manually reload them</p><ol><li><p>Sadly, <code>HotwireTabBarController</code> (yet?) doesn&#8217;t expose that</p></li></ol></li><li><p>Add a &#8220;stale&#8221; flag so the tab refreshes the next time it appears</p></li></ol><p>Adam shared how Sleep HQ tackles this by reloading the entire path configuration when users switch between &#8220;patient&#8221; and &#8220;clinician&#8221; modes. It works, but it&#8217;s heavy-handed. On Android, there&#8217;s an additional constraint: you simply can&#8217;t have an unlimited number of tabs.</p><p>For <a href="https://jumpstartrails.com/ios">Jumpstart Pro</a>, we&#8217;ve moved away from path configuration for tabs entirely. Instead, we send a dynamic JSON structure down to a bridge component, the same approach I outlined in <a href="https://newsletter.masilotti.com/p/hotwire-native-deep-dive-authentication">my authentication deep dive</a>. It gives you control without rebuilding the whole navigation stack.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;bf5b8c09-7603-4ec3-a29e-c179e9cc73b8&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;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Hotwire Native deep dive: Authentication&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;Hotwire Native consultant and author obsessed with building mobile apps powered by Ruby on Rails. Here to prove you can run a thriving independent business without sacrificing family time.&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;:12,&quot;comment_count&quot;:11,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Hotwire Native Weekly&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><h3><strong>&#8220;Do you have a starter kit for new apps?&#8221;</strong></h3><p>Jesse asked whether there&#8217;s a Rails + iOS + Android template that people can use as a starting point.</p><p>And I&#8217;ve tried&#8230; many times. Six, at least. Templates are great until they aren&#8217;t! They fall out of date the moment Rails or Hotwire or Xcode moves forward.</p><p>The pattern that has consistently worked for me is:</p><ul><li><p>Keep a repo with multiple branches representing different &#8220;starting points&#8221;</p></li><li><p>Write generators that you run <em>after</em> creating a new Rails app</p></li></ul><p>This gives me 90% of the value of a template without locking into something brittle.</p><h3><strong>Going &#8220;fully native&#8221; vs staying with Hotwire Native</strong></h3><p>I also shared a little experiment I&#8217;ve been working on: I rewrote the hiking app from <a href="https://newsletter.masilotti.com/p/hotwire-native-for-rails-developers">my book</a> in pure SwiftUI. The goal was to compare the experience head-to-head.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;152a2900-a7b7-4e72-9cdf-8c6c012f3e71&quot;,&quot;caption&quot;:&quot;After more than two years of writing, editing, and rewriting (and almost a decade of building Hotwire Native apps) my book is finally out of beta and on bookshelves.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Hotwire Native for Rails Developers&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;Hotwire Native consultant and author obsessed with building mobile apps powered by Ruby on Rails. Here to prove you can run a thriving independent business without sacrificing family time.&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-09-23T23:23:26.602Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!tyAc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa01a0993-4273-4b74-a56a-f63f8239cd3d_4032x3024.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/hotwire-native-for-rails-developers&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:174389180,&quot;type&quot;:&quot;page&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Hotwire Native Weekly&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><p>Here&#8217;s what I learned:</p><ul><li><p>Visually, the difference is tiny, in many cases you couldn&#8217;t tell which is which</p></li><li><p>The native version requires <strong>significantly</strong> more code</p></li><li><p>Error states and loading transitions are easier to customize natively, but not enough to justify the effort unless you truly need it</p></li><li><p>Mixing in native screens when necessary (e.g., Bluetooth, camera, biometrics) is still the sweet spot</p></li></ul><p>In other words: <strong>if you&#8217;re building an app whose UI comes from Rails anyway, Hotwire Native continues to be the fastest path</strong>, even if you know SwiftUI well.</p><p>We also talked about custom loading and skeleton screens. You can do this by inflating JavaScript-driven views early, or by putting the whole app inside a Turbo Frame remote and swapping in real content after it loads. Adam shared a clever technique where he checks whether a page is cached before showing the skeleton, avoiding the &#8220;flash&#8221; when navigating back.</p><h3><strong>A great way to wrap up the year</strong></h3><p>This session had everything I love about office hours: real problems, real apps, and everyone openly sharing what&#8217;s worked (and what hasn&#8217;t). Huge thanks to Adam for the Bluetooth demo, to Shawn and Jesse for great questions, and to everyone who joined from far-off time zones.</p><p>The next session is scheduled for January 8, 2026. I hope to see you there!</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;b9c7c8d7-a2cd-4b1b-a626-d9b30fa42527&quot;,&quot;caption&quot;:&quot;Every month I host an hour-long Zoom session for Hotwire Native developers to ask questions, share progress, and connect with others building the same way.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Hotwire Native Office Hours&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:16209564,&quot;name&quot;:&quot;Joe Masilotti&quot;,&quot;bio&quot;:&quot;Hotwire Native consultant and author obsessed with building mobile apps powered by Ruby on Rails. Here to prove you can run a thriving independent business without sacrificing family time.&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-09-17T00:24:39.338Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!Z9wc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1475c5dd-6c17-4d31-a80f-cf975c7cfcc7_4032x2565.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.masilotti.com/p/office-hours&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:173808983,&quot;type&quot;:&quot;page&quot;,&quot;reaction_count&quot;:2,&quot;comment_count&quot;:0,&quot;publication_id&quot;:6282257,&quot;publication_name&quot;:&quot;Hotwire Native Weekly&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>]]></content:encoded></item></channel></rss>