I Built a Production iOS App With Rails and No Swift Developer
Why Hotwire Native ships mobile apps faster than React Native—and when you actually need a native framework
Most teams waste months building separate mobile APIs when their Rails app could ship iOS and Android apps directly. Here's what breaks with Hotwire Native, when it's genuinely wrong, and why cross-platform frameworks solve problems you don't have.
Jay McBride
Software Engineer
Introduction: The Mobile App That Shipped in Three Weeks
A client needed an iOS app. They had a working Rails application with all the business logic, authentication, and data models. They budgeted four months and $60k for “a native mobile experience.”
I shipped it in three weeks with Hotwire Native. No Swift developer. No separate API layer. No synchronization bugs between web and mobile.
The app passed App Store review on the first submission. Users couldn’t tell it wasn’t “fully native.” The client saved $45k and launched two months ahead of schedule.
Hotwire Native isn’t a framework. It’s Rails running inside a native shell. Your web views become screens. Your existing HTML becomes the UI. Navigation, authentication, and state management all work exactly like your web app—because they are your web app.
This article is for teams who’ve built a React Native app, maintained two codebases, and wondered why simple features take twice as long to ship. If you haven’t felt the pain of keeping mobile and web in sync, come back after your third “API schema changed but mobile still expects the old format” bug.
Enjoying this? 👉 Tip a coffee and keep posts coming
Here’s what actually works in production—and what breaks when you push it too far.
The Core Judgment: Ship One Codebase, Not Three
Most teams building mobile apps start with this architecture:
- Backend API (Rails, Django, whatever)
- Web frontend (React, Vue, or server-rendered)
- Mobile app (React Native, Flutter)
Every feature gets built three times. API endpoint, web UI, mobile UI. Every bug gets fixed three times. Every design change gets implemented three times.
Hotwire Native collapses this to one codebase. Your Rails app serves HTML. Turbo Native (the iOS/Android shells) wraps your web views in a native navigation container. Features you build for web work in mobile automatically.
I default to Hotwire Native for any mobile app that’s primarily content and forms—which describes 80% of business applications. Customer portals, admin tools, e-commerce checkouts, social feeds. If your mobile app is “the web version but on a phone,” Hotwire Native ships it faster.
Teams reach for React Native or Flutter because they assume native development is complex. It is. But Hotwire Native isn’t native development. It’s web development that feels native.
How This Works in the Real World
Hotwire Native gives you three primitives:
- Turbo for navigation: Links and forms drive navigation like a SPA, but each screen is a separate web view with native transitions.
- Strada for native bridges: When you need actual native features (camera, push notifications, biometrics), Strada provides JavaScript-to-native communication.
- Native chrome: The tab bar, navigation stack, and system gestures are real native UI. Your HTML is the content.
When a user taps a link in your Rails app, Turbo intercepts it, fetches the HTML, and pushes a new screen with a native slide animation. The URL changes. The browser back button becomes the native back button. Deep linking works automatically because every screen is just a URL.
What Actually Breaks
Hotwire Native falls apart when:
- You need complex gestures (pinch-to-zoom maps, canvas-based drawing)
- Your app works offline with local data storage
- You’re building games or media-heavy experiences
- You need frame-perfect animations or 60fps interactions
In those cases, React Native or true native development makes sense. For everything else, you’re choosing maintenance burden over marginal polish.
A Real Example: Field Service App Rebuild
I rebuilt a field service app for a logistics company. Technicians used it to view job details, upload photos, and mark tasks complete. The original React Native version required separate iOS and Android developers, a backend team maintaining GraphQL APIs, and constant synchronization bugs.
Original architecture:
- Rails backend with GraphQL API
- React Native mobile app (12,000 lines of TypeScript)
- Three developers maintaining it
- Average feature: 2 weeks from spec to production
Hotwire Native rebuild:
- Same Rails backend, removed GraphQL, added Turbo
- iOS/Android shells (300 lines each, mostly configuration)
- One Rails developer maintaining it
- Average feature: 3 days from spec to production
The functionality was identical. Photo uploads used native camera with Strada bridge. Offline support used HTTP caching headers (ServiceWorker on web, URL cache on mobile). Push notifications used standard Rails ActionCable with native presentation.
What surprised me:
Performance was better. No JavaScript bundle to parse. No React reconciliation overhead. Just server-rendered HTML that appeared instantly.
Native features were easier than expected. Strada’s bridge pattern meant I wrote 20 lines of Swift to expose the camera, then called it from Rails with a JavaScript component. Simpler than React Native’s module system.
App Store approval was trivial. Apple cares about user experience, not implementation. The app felt native because navigation, gestures, and system integration were native.
Common Mistakes I Keep Seeing
Treating it like a WebView wrapper. Hotwire Native is not PhoneGap. Navigation happens natively. Each screen is a discrete view controller. The user can swipe back. The app works with iOS gestures. If you’re just embedding your website in a frame, you’re doing it wrong.
Ignoring mobile-specific UI. Desktop layouts don’t work on phones. Use responsive design, but also consider creating mobile-specific views (responds_to :turbo_native_ios in Rails). Your forms need bigger touch targets. Your tables need to stack vertically.
Over-using Strada bridges. Most features don’t need native code. Before writing Swift, ask: “Can I do this with HTML and CSS?” File uploads, form validation, and animations all work in web views. Save bridges for camera, location, and system APIs.
Not testing on real devices. Simulators lie. Network conditions, memory pressure, and touch interactions behave differently on actual iPhones. Test on hardware early.
Tradeoffs and When This Is Actually Wrong
You’re Tied to Server Latency
Every screen loads from the server. If your backend is slow or users have poor connectivity, the app feels sluggish.
Mitigation: aggressive caching (Turbo handles this automatically), optimistic UI updates with Turbo Frames, and offline fallbacks with ServiceWorker for web + URL cache for mobile.
Complex Client-Side State Is Painful
Hotwire Native excels at request-response patterns. It struggles with rich client-side interactions that React or Flutter handle elegantly.
If your app is mostly forms and content, perfect. If your app is Figma or Notion, choose React Native.
App Store Rejections for “Just a Website”
Apple has rejected apps that are “just a web wrapper” with no native features or offline functionality. Hotwire Native avoids this because navigation is native and you can add Strada bridges for system integration.
But if your app is literally your marketing site in a shell, Apple will reject it. Add value: push notifications, camera integration, offline support, or native chrome.
Best Practices I Actually Follow
Start with web, add mobile later. Build your Rails app with responsive design. Test on mobile browsers. When you’re ready for App Store distribution, add Turbo Native shells. This takes hours, not weeks.
Use Turbo Frames for surgical updates. Don’t reload entire screens when a form submits. Target specific frames. This feels like SPA interactivity but requires zero JavaScript.
Cache aggressively. Turbo Native caches responses automatically. Configure your Rails cache headers properly. Users should rarely see loading spinners on repeated views.
Create mobile-specific controllers. Detect request.user_agent.include?("Turbo Native") and serve simplified layouts. Mobile users don’t need sidebars or desktop navigation chrome.
Test authentication flows carefully. Session cookies work differently in web views. Use SameSite=Lax, test deep linking after logout, and handle token expiration gracefully.
When to Choose Hotwire Native vs. React Native vs. Flutter
Choose Hotwire Native when:
- You already have a Rails (or Django, Laravel) app
- Your mobile app is primarily forms, content, and standard interactions
- Your team is backend-heavy and wants to avoid mobile specialists
- You need to ship fast and maintain one codebase
Choose React Native when:
- You need complex client-side state management
- Your app requires custom gestures and animations
- You’re building a standalone mobile app with no web equivalent
- Your team is JavaScript-heavy and React-experienced
Choose Flutter when:
- You need pixel-perfect custom UI across platforms
- You’re building a design-heavy consumer app
- You have performance requirements beyond what web views provide
- You want strong compile-time type safety
Choose true native (Swift/Kotlin) when:
- You’re building platform-specific features (HealthKit, ARKit)
- You need maximum performance (games, AR, complex media)
- You’re investing in long-term platform expertise
The honest answer: if you’re reading this article and your app doesn’t need real-time collaboration or complex animations, Hotwire Native probably ships it faster.
Real Limitations I’ve Hit in Production
Memory usage is higher. Each screen is a web view with a full browser engine. iOS manages this well, but you can hit memory warnings with deep navigation stacks. Implement proper cleanup in your Turbo Native shell.
Debugging is harder. You’re debugging three layers: Rails, HTML/CSS, and native navigation. Use Safari Web Inspector for the web views, Xcode for native code, and Rails logs for backend issues.
Some JavaScript libraries break. Libraries that assume a persistent DOM (some charting libraries, complex state managers) break when Turbo Drive caches pages. Use Turbo-compatible libraries or disable Turbo on specific pages.
Native developers will hate it. If your team includes experienced iOS developers, they’ll find Hotwire Native limiting. They’re right—for the apps they’re used to building. But most business apps aren’t Instagram.
Conclusion: One Codebase, Three Platforms
React Native and Flutter solve a real problem: building mobile apps without separate iOS and Android teams. But they create a new problem: maintaining separate mobile and web codebases.
Hotwire Native solves the actual problem most teams have: shipping a mobile app without slowing down web development.
Your Rails app becomes your iOS app becomes your Android app. Features ship once. Bugs fix once. Designs change once.
The future isn’t “should we build mobile?” It’s “why are we maintaining separate codebases when our users just want access on their phones?”
Start with responsive web design. Add Turbo Native shells when you need App Store distribution. Use Strada bridges only for actual native features.
Your users don’t care about your tech stack. They care about features that work.
Frequently Asked Questions (FAQs)
Does this work with Android?
Yes. Turbo Android works the same way as Turbo iOS. You write Kotlin instead of Swift for native bridges, but the HTML and Rails code is identical. One backend, two thin native shells.
What about React instead of Rails?
Turbo Native works with any backend that serves HTML. Next.js, Remix, and Django all work. But Rails has the best integration because Hotwire (Turbo + Stimulus) was built by the Rails community.
Can I use native UI components?
Yes, via Strada bridges. You can replace HTML sections with native components (like a native image picker instead of HTML file input). But this adds complexity. Start with HTML, add native components only when necessary.
How do I handle deep linking?
Every screen is a URL. Deep linking is just routing. iOS universal links point to your Rails routes. Turbo Native intercepts them and navigates to the right screen automatically.
What about push notifications?
Use standard Rails ActionCable or a push service (OneSignal, FCM). The notification payload includes a URL. Tapping it deep links into your app. Turbo Native handles the navigation.
Does Apple approve these apps?
Yes, if you provide real value. Add native chrome (tab bars, proper navigation), implement offline support, use Strada bridges for system features, and provide a better experience than mobile web. Apple rejects lazy web wrappers, not well-built Turbo Native apps.
Your turn: How many features have you built twice—once for web, once for mobile—that could’ve shipped once with Hotwire Native?
Enjoying this? 👉 Tip a coffee and keep posts coming
