I still remember the knot in my stomach as we watched the demo.
We were standing in a converted barn in rural Vermont, holding our iPads up to show the client their brand‑new field service app. The barn had beautiful wooden beams, a lot of charm, and exactly zero bars of cellular signal. The app, a sleek Turbo Native wrapper around our Rails web app had loaded perfectly when we tested it in the office. But out here, with no network, it just showed a white screen and a spinning spinner that would never stop.
The client smiled politely. I wanted to disappear into the hay.
That day taught me something that no amount of architectural diagrams could have conveyed: Offline is not a feature. It’s a promise you make to your users. And if you’re building a hybrid app with Turbo Native, you’d better have a strategy to keep that promise when the world goes dark.
The Dream and the Reality
Turbo Native is a beautiful piece of technology. For those who haven’t used it: it’s part of the Hotwire ecosystem, designed to let you wrap your existing web app in a native shell, giving you the best of both worlds
native navigation and the flexibility of the web. You build one set of views in Rails (or any backend), and they render inside a native WebView. It’s fast, it’s elegant, and it makes senior full‑stack developers feel like they’ve found a cheat code.
But there’s a catch.
Turbo Native, out of the box, assumes you have a network. It loads URLs, caches them briefly, but if you’re offline, it fails. Gracefully? Not really. It fails with the same blank despair my iPad showed in that barn.
The client’s technicians would be working in basements, parking garages, and yes, rural barns. They needed to use the app view their work orders, fill out forms, take photos even when the network was a distant memory.
We needed an offline‑first strategy. And that’s when I rediscovered a tool I had previously dismissed as “just for marketing sites”: Workbox.
Workbox: Not Just for SPAs
If you’ve only seen Workbox used to precache static assets for a React app, you’re missing the real magic. Workbox is a library that sits on top of service workers, and service workers are the most underrated API on the web platform. They are a proxy between your app and the network, and when you combine them with Turbo Native, you can turn your hybrid app into a resilient, offline‑first machine.
But let’s be honest: service workers are also a pain to get right. They live in a weird space, they update in mysterious ways, and debugging them can make you question your career choices. Workbox abstracts the complexity into declarative strategies, but you still need to think like an artist not an assembly line worker.
The Art of Caching Strategy
The first mistake we made was thinking we could just precache everything. Throw the entire web app into the service worker at install time. That works for a simple site, but our app had thousands of work orders, user‑specific data, and a backend that changed daily. Precaching all of that would have been insane.
So we had to think about what to cache and how.
1. Static Assets: Cache‑First with a Fallback
The app shell CSS, JavaScript, images should be available offline. For these, we used Workbox’s StaleWhileRevalidate strategy. Users get the cached version instantly, and the service worker quietly updates it in the background. This gives the illusion of speed and resilience.
workbox.routing.registerRoute(
({request}) => request.destination === 'style' || request.destination === 'script',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
2. API Responses: Network‑First with a Cache Fallback
For dynamic data like work orders, we used NetworkFirst. Try the network; if it fails, serve from cache. But here’s the art: you need to decide which requests get this treatment. For us, the home dashboard and individual work orders were critical. We also had to handle pagination and search caching every possible query would be wasteful.
We ended up with a hybrid: we cached the last‑viewed work orders and used a custom cache key based on the URL and the user ID. Workbox’s plugins allowed us to add custom logic.
3. Offline Writes: The Hardest Part
The real complexity came with mutations. Technicians needed to submit forms (complete a work order, add notes, take photos) even when offline. How do you handle that?
We initially tried to rely on the browser’s built‑in form submission, but it would fail and show an error. Not acceptable. So we built a tiny client‑side queue using IndexedDB. When the user submits a form offline, we intercept the request, store it in IndexedDB, and immediately update the UI optimistically. When the network returns, we replay the requests in order using Workbox’s background sync.
This part felt like surgery. We had to ensure idempotency, conflict resolution, and a user‑friendly UI that showed “pending sync” indicators. But when it worked when we could fill out a form in a dead zone, drive to a Starbucks, and watch the data silently sync it was pure magic.
The Turbo Native Twist
Here’s where it gets interesting. Turbo Native has its own navigation stack. It loads pages via Turbo.visit() and caches them in a memory‑based cache. If you’re offline, that cache is empty, and the app fails.
We had to make the service worker and Turbo work together. The key was to intercept requests at the service worker level before Turbo even sees them. If the service worker returns a cached response, Turbo thinks it came from the network. That means the entire navigation experience remains smooth, even offline.
But there was a gotcha: Turbo uses fetch for its requests, and service workers can respond to those. However, Turbo also maintains its own back‑forward cache. We had to be careful not to double‑cache or cause conflicts. The solution was to keep Turbo’s in‑memory cache short‑lived (which is default) and rely on the service worker for long‑term offline storage.
We also used Workbox’s NavigationRoute to handle the initial page loads, ensuring that the app shell was always available.
The Journey: From Panic to Pride
Looking back, the journey to offline‑first Turbo Native was not a straight line. It was a series of failures, each one teaching us something new:
- We broke the service worker update flow and users were stuck on an old version. Learned to implement a version‑based cache busting and prompt users to refresh.
- We cached API responses that contained sensitive data, then realized we needed to clear the cache on logout. Added a custom cache cleanup.
- We tried to sync offline mutations without proper ordering, causing conflicts. Moved to a serial queue with retry logic.
But the moment that made it all worth it was when we returned to that barn. This time, we had the app open, and we deliberately turned on airplane mode. The app still showed the dashboard cached data. We clicked into a work order, added notes, attached a photo, and hit “Submit.” It showed “Saved offline.” Then we turned off airplane mode, and within seconds, the data appeared on our server.
The client’s eyes lit up. “So it just works? Anywhere?”
“Yes,” I said. “Anywhere.”
The Bigger Picture: Art, Not Engineering
What we built wasn’t just a technical solution. It was a piece of art. We had to understand the users’ context working in the field, in unpredictable conditions and shape the technology to fit their reality, not the other way around.
Offline‑first is an attitude. It’s about assuming the network is unreliable, and designing for that as the default. Service workers and Workbox give you the palette, but the composition is yours.
For senior full‑stack developers, this is the kind of work that matters. It’s not about following a recipe; it’s about understanding the medium web views, service workers, native shells and blending them into something seamless.
Your Turn
If you’re building a Turbo Native app and you haven’t yet considered offline, I urge you to. Start small: cache your static assets, then move to API responses, then tackle mutations. Embrace the complexity, because the reward is an app that works in elevators, on airplanes, and in the middle of nowhere.
And when you inevitably hit a wall, remember: the service worker is just a JavaScript file. You can debug it, you can version it, you can bend it to your will. It’s not magic, it’s just code. But the experience it enables? That’s the magic.
Top comments (0)