From 5d084a46ce58217bde19302dd1044a693c766177 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 2 Jan 2026 19:25:47 +0100
Subject: [PATCH 01/18] Switch to turbo-rails version with offline mode support
---
Gemfile | 2 +-
Gemfile.lock | 14 ++++++++++----
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/Gemfile b/Gemfile
index 17e7496c0..da66e1473 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,7 +8,7 @@ gem "rails", github: "rails/rails", branch: "main"
gem "importmap-rails"
gem "propshaft"
gem "stimulus-rails"
-gem "turbo-rails"
+gem "turbo-rails", github: "hotwired/turbo-rails", branch: "offline-cache"
# Deployment and drivers
gem "bootsnap", require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 3f8461b39..3a35c35c7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -11,6 +11,15 @@ GIT
specs:
useragent (0.16.11)
+GIT
+ remote: https://github.com/hotwired/turbo-rails.git
+ revision: e3795ef6aac6a3d18039751febe6a301cfe71a7b
+ branch: offline-cache
+ specs:
+ turbo-rails (2.0.20)
+ actionpack (>= 7.1.0)
+ railties (>= 7.1.0)
+
GIT
remote: https://github.com/rails/rails.git
revision: 12e24eaf2f0a9613e015653f013dd131317d9bf5
@@ -461,9 +470,6 @@ GEM
trilogy (2.10.0)
bigdecimal
tsort (0.2.0)
- turbo-rails (2.0.23)
- actionpack (>= 7.1.0)
- railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
@@ -539,7 +545,7 @@ DEPENDENCIES
stimulus-rails
thruster
trilogy (~> 2.10)
- turbo-rails
+ turbo-rails!
useragent!
vcr
web-console!
From ada4688c4e0bc2a6adcdd757821d1779ed4cd570 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Mon, 5 Jan 2026 12:10:57 +0100
Subject: [PATCH 02/18] Take a first stab at offline mode support
---
Gemfile.lock | 4 +-
Gemfile.saas.lock | 14 +++--
app/javascript/initializers/index.js | 1 +
app/javascript/initializers/offline.js | 6 ++
app/views/pwa/service_worker.js | 41 --------------
app/views/pwa/service_worker.js.erb | 76 ++++++++++++++++++++++++++
config/importmap.rb | 1 +
7 files changed, 96 insertions(+), 47 deletions(-)
create mode 100644 app/javascript/initializers/offline.js
delete mode 100644 app/views/pwa/service_worker.js
create mode 100644 app/views/pwa/service_worker.js.erb
diff --git a/Gemfile.lock b/Gemfile.lock
index 3a35c35c7..a710d0f9e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,10 +13,10 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: e3795ef6aac6a3d18039751febe6a301cfe71a7b
+ revision: 49be3f2d43edeeedad48cb492b35242fdb27af74
branch: offline-cache
specs:
- turbo-rails (2.0.20)
+ turbo-rails (2.0.21)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index 152d1889c..cec5be07b 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -60,6 +60,15 @@ GIT
rails (>= 6.1)
yabeda (~> 0.6)
+GIT
+ remote: https://github.com/hotwired/turbo-rails.git
+ revision: 49be3f2d43edeeedad48cb492b35242fdb27af74
+ branch: offline-cache
+ specs:
+ turbo-rails (2.0.21)
+ actionpack (>= 7.1.0)
+ railties (>= 7.1.0)
+
GIT
remote: https://github.com/rails/rails.git
revision: 12e24eaf2f0a9613e015653f013dd131317d9bf5
@@ -633,9 +642,6 @@ GEM
trilogy (2.10.0)
bigdecimal
tsort (0.2.0)
- turbo-rails (2.0.23)
- actionpack (>= 7.1.0)
- railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
@@ -752,7 +758,7 @@ DEPENDENCIES
stripe (~> 18.0)
thruster
trilogy (~> 2.10)
- turbo-rails
+ turbo-rails!
useragent!
vcr
web-console!
diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js
index 90ef36a26..1d16ef205 100644
--- a/app/javascript/initializers/index.js
+++ b/app/javascript/initializers/index.js
@@ -1,2 +1,3 @@
import "initializers/current"
import "initializers/bridge/bridge_element"
+import "initializers/offline"
diff --git a/app/javascript/initializers/offline.js b/app/javascript/initializers/offline.js
new file mode 100644
index 000000000..c5eff824e
--- /dev/null
+++ b/app/javascript/initializers/offline.js
@@ -0,0 +1,6 @@
+import { Turbo } from "@hotwired/turbo-rails"
+
+Turbo.offline.start("/service-worker.js", {
+ scope: "/",
+ native: true
+})
diff --git a/app/views/pwa/service_worker.js b/app/views/pwa/service_worker.js
deleted file mode 100644
index 91903e735..000000000
--- a/app/views/pwa/service_worker.js
+++ /dev/null
@@ -1,41 +0,0 @@
-self.addEventListener('fetch', (event) => {
- if (event.request.method !== 'GET') return
-
- if (event.request.destination === 'document') {
- event.respondWith(
- fetch(event.request, { cache: 'no-cache' })
- .catch(() => caches.match(event.request))
- )
- }
-})
-
-self.addEventListener("push", (event) => {
- const data = event.data.json()
- event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ]))
-})
-
-async function showNotification({ title, options }) {
- return self.registration.showNotification(title, options)
-}
-
-async function updateBadgeCount({ data: { badge } }) {
- return self.navigator.setAppBadge?.(badge || 0)
-}
-
-self.addEventListener("notificationclick", (event) => {
- event.notification.close()
-
- const url = new URL(event.notification.data.url, self.location.origin).href
- event.waitUntil(openURL(url))
-})
-
-async function openURL(url) {
- const clients = await self.clients.matchAll({ type: "window" })
- const focused = clients.find((client) => client.focused)
-
- if (focused) {
- await focused.navigate(url)
- } else {
- await self.clients.openWindow(url)
- }
-}
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
new file mode 100644
index 000000000..fb583658c
--- /dev/null
+++ b/app/views/pwa/service_worker.js.erb
@@ -0,0 +1,76 @@
+importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
+
+TurboOffline.addRule({
+ match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
+ handler: TurboOffline.handlers.cacheFirst({
+ cacheName: "assets",
+ maxAge: 60 * 60 * 24 * 7
+ })
+})
+
+TurboOffline.addRule({
+ match: (request) => {
+ const url = request.url
+ return /\/(boards|cards|users)\//.test(url) && !/\/(edit|pin|watch|new)$/.test(url)
+ },
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "cards",
+ maxAge: 60 * 60 * 24 * 3,
+ networkTimeout: 3
+ })
+})
+
+TurboOffline.addRule({
+ match: /\/rails\/active_storage\//,
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "storage",
+ maxAge: 60 * 60 * 24 * 7,
+ networkTimeout: 3
+ })
+})
+
+// Everything else
+TurboOffline.addRule({
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "misc",
+ maxAge: 60 * 60 * 24,
+ networkTimeout: 3
+ })
+})
+
+TurboOffline.start()
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener("push", (event) => {
+ const data = event.data.json()
+ event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ]))
+})
+
+async function showNotification({ title, options }) {
+ return self.registration.showNotification(title, options)
+}
+
+async function updateBadgeCount({ data: { badge } }) {
+ return self.navigator.setAppBadge?.(badge || 0)
+}
+
+self.addEventListener("notificationclick", (event) => {
+ event.notification.close()
+
+ const url = new URL(event.notification.data.url, self.location.origin).href
+ event.waitUntil(openURL(url))
+})
+
+async function openURL(url) {
+ const clients = await self.clients.matchAll({ type: "window" })
+ const focused = clients.find((client) => client.focused)
+
+ if (focused) {
+ await focused.navigate(url)
+ } else {
+ await self.clients.openWindow(url)
+ }
+}
diff --git a/config/importmap.rb b/config/importmap.rb
index 5da84683c..f6910c3e1 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -2,6 +2,7 @@
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
+pin "@hotwired/turbo/offline", to: "turbo-offline.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin "@hotwired/hotwire-native-bridge", to: "@hotwired--hotwire-native-bridge.js"
From 9fc724572e900953ca500178e3615ec19b7e8602 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Tue, 20 Jan 2026 10:53:40 +0100
Subject: [PATCH 03/18] Use new `except` option for offline rules
---
app/views/pwa/service_worker.js.erb | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index fb583658c..0c4ab6ea5 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -9,14 +9,12 @@ TurboOffline.addRule({
})
TurboOffline.addRule({
- match: (request) => {
- const url = request.url
- return /\/(boards|cards|users)\//.test(url) && !/\/(edit|pin|watch|new)$/.test(url)
- },
+ match: /\/(boards|cards|users)\//,
+ except: /\/(edit|pin|watch|new)$/,
handler: TurboOffline.handlers.networkFirst({
cacheName: "cards",
maxAge: 60 * 60 * 24 * 3,
- networkTimeout: 3
+ networkTimeout: 2
})
})
@@ -25,7 +23,7 @@ TurboOffline.addRule({
handler: TurboOffline.handlers.networkFirst({
cacheName: "storage",
maxAge: 60 * 60 * 24 * 7,
- networkTimeout: 3
+ networkTimeout: 2
})
})
From 975968b50f9c8033e278460736e82bbfc2b3dec2 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Tue, 20 Jan 2026 19:16:17 +0100
Subject: [PATCH 04/18] Only enable offline caching for Hotwire Native apps
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This change conditionally renders the TurboOffline caching rules based on
whether the request comes from a Hotwire Native app. Web browsers get a
minimal service worker that only handles push notifications and a simple
document fetch fallback.
Why separate behavior for native vs web?
-----------------------------------------
We want offline caching for Hotwire Native apps (where users expect
app-like offline behavior) but not for regular web browsers.
Why use ERB conditional rendering?
----------------------------------
We explored several approaches to detect Hotwire Native requests in the
service worker:
1. User-Agent detection in fetch handler: Would be ideal, but Android
WebViews override the User-Agent header with the system default when
requests are intercepted by service workers, stripping the custom
"Hotwire Native" identifier.
2. Custom header (X-Hotwire-Native) from native apps: Would require
intercepting ALL requests at the native level using both
WebViewClient.shouldInterceptRequest() and ServiceWorkerClient.
Complex to implement and still incomplete coverage for all request
types (navigation, resources loaded by HTML).
3. In-memory flag with IndexedDB persistence: Service workers can be
terminated when idle and restart with fresh state. Reading from
IndexedDB is async, but the decision to call respondWith() in a
fetch handler must be synchronous.
4. Separate service worker URLs: Same theoretical churn problem as ERB
rendering, with more complexity.
Why ERB conditional rendering works in practice
-----------------------------------------------
The main concern with conditional ERB rendering was "churn" — the service
worker constantly updating as different client types fetch it. However,
this only happens when web and native share storage on the same device.
In practice, this is rare because:
- Android native apps use isolated WebView storage
- iOS doesn't support service workers in WebViews (yet)
- Web browsers have completely separate storage
So each context gets its own stable service worker without churn.
Co-Authored-By: Claude Opus 4.5
---
app/views/pwa/service_worker.js.erb | 73 +++++++++++++++++------------
1 file changed, 43 insertions(+), 30 deletions(-)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index 0c4ab6ea5..b3f59aa60 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -1,44 +1,57 @@
importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
-TurboOffline.addRule({
- match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
- handler: TurboOffline.handlers.cacheFirst({
- cacheName: "assets",
- maxAge: 60 * 60 * 24 * 7
+<% if hotwire_native_app? %>
+ TurboOffline.addRule({
+ match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
+ handler: TurboOffline.handlers.cacheFirst({
+ cacheName: "assets",
+ maxAge: 60 * 60 * 24 * 7
+ })
})
-})
-TurboOffline.addRule({
- match: /\/(boards|cards|users)\//,
- except: /\/(edit|pin|watch|new)$/,
- handler: TurboOffline.handlers.networkFirst({
- cacheName: "cards",
- maxAge: 60 * 60 * 24 * 3,
- networkTimeout: 2
+ TurboOffline.addRule({
+ match: /\/(boards|cards|users)\//,
+ except: /\/(edit|pin|watch|new)$/,
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "cards",
+ maxAge: 60 * 60 * 24 * 3,
+ networkTimeout: 2
+ })
})
-})
-TurboOffline.addRule({
- match: /\/rails\/active_storage\//,
- handler: TurboOffline.handlers.networkFirst({
- cacheName: "storage",
- maxAge: 60 * 60 * 24 * 7,
- networkTimeout: 2
+ TurboOffline.addRule({
+ match: /\/rails\/active_storage\//,
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "storage",
+ maxAge: 60 * 60 * 24 * 7,
+ networkTimeout: 2
+ })
})
-})
-// Everything else
-TurboOffline.addRule({
- handler: TurboOffline.handlers.networkFirst({
- cacheName: "misc",
- maxAge: 60 * 60 * 24,
- networkTimeout: 3
+ // Everything else
+ TurboOffline.addRule({
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "misc",
+ maxAge: 60 * 60 * 24,
+ networkTimeout: 3
+ })
})
-})
-TurboOffline.start()
+ TurboOffline.start()
+<% else %>
+ self.addEventListener("fetch", (event) => {
+ if (event.request.method !== "GET") return
-self.addEventListener('activate', (event) => {
+ if (event.request.destination === "document") {
+ event.respondWith(
+ fetch(event.request, { cache: "no-cache" })
+ .catch(() => caches.match(event.request))
+ )
+ }
+ })
+<% end %>
+
+self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim())
})
From a949f1e3aed45b89e8a8a9caa6f486c858f87acf Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Tue, 27 Jan 2026 09:32:15 +0100
Subject: [PATCH 05/18] Clear offline cache when logging out
Adds a logout Stimulus controller that sends a message to the service
worker to clear all cached content when the user logs out. This ensures
that cached data from one user isn't accessible after logout.
The implementation:
- Adds logout_controller.js that posts { action: "clearCache" } to the
service worker via postMessage
- Updates logout buttons to use the controller on form submission
Also fixes data-turbo placement: moved from button to form element where
it actually takes effect for disabling Turbo Drive form submissions.
Co-Authored-By: Claude Opus 4.5
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
app/javascript/controllers/logout_controller.js | 8 ++++++++
app/views/my/menus/_settings.html.erb | 2 +-
app/views/users/show.html.erb | 2 +-
5 files changed, 12 insertions(+), 4 deletions(-)
create mode 100644 app/javascript/controllers/logout_controller.js
diff --git a/Gemfile.lock b/Gemfile.lock
index a710d0f9e..a17769c07 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 49be3f2d43edeeedad48cb492b35242fdb27af74
+ revision: 45a2a40be0a677808a3a269311f1b5abaa3b30ba
branch: offline-cache
specs:
turbo-rails (2.0.21)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index cec5be07b..03c8e9141 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 49be3f2d43edeeedad48cb492b35242fdb27af74
+ revision: 45a2a40be0a677808a3a269311f1b5abaa3b30ba
branch: offline-cache
specs:
turbo-rails (2.0.21)
diff --git a/app/javascript/controllers/logout_controller.js b/app/javascript/controllers/logout_controller.js
new file mode 100644
index 000000000..bc0511a7f
--- /dev/null
+++ b/app/javascript/controllers/logout_controller.js
@@ -0,0 +1,8 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ async clearCache() {
+ const registration = await navigator.serviceWorker?.ready
+ registration?.active?.postMessage({ action: "clearCache" })
+ }
+}
diff --git a/app/views/my/menus/_settings.html.erb b/app/views/my/menus/_settings.html.erb
index d1251b7c0..6b711544b 100644
--- a/app/views/my/menus/_settings.html.erb
+++ b/app/views/my/menus/_settings.html.erb
@@ -6,7 +6,7 @@
<%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
<%= icon_tag "logout", class: "popup__icon" %>
- <%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %>
+ <%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", form: { data: { turbo: false, controller: "logout", action: "submit->logout#clearCache" } } do %>
Sign out
<% end %>
<% end %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index 58068d2d6..a285a4f5a 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -41,7 +41,7 @@
<% if Current.user == @user %>
- <%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
+ <%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", form: { data: { turbo: false, controller: "logout", action: "submit->logout#clearCache" } } do %>
Sign out of Fizzy on this device
<% end %>
<% end %>
From cf8d92afeaf031980296da10340674477c339f4f Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Wed, 28 Jan 2026 21:30:50 +0100
Subject: [PATCH 06/18] Enable offline caching for all web users
Previously, offline caching was conditionally enabled only for Hotwire
Native apps. This removes that restriction and enables offline support
for all users, including PWAs and regular browsers.
This Uses the new `fetchOptions` support in `TurboOffline` handlers
to pass `cache: "no-cache"` for document fetches, which we need
to work around a quite annoying Safari PWA bug
(see https://github.com/basecamp/fizzy/pull/1014).
Also, simplify a bit the cache names and remove the `misc` one.
Co-Authored-By: Claude Opus 4.5
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
app/views/pwa/service_worker.js.erb | 83 +++++++++++++++--------------
3 files changed, 44 insertions(+), 43 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index a17769c07..3cad12c54 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 45a2a40be0a677808a3a269311f1b5abaa3b30ba
+ revision: 9bcc244043b16ef0202f8fe93aaeb246d841f9eb
branch: offline-cache
specs:
turbo-rails (2.0.21)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index 03c8e9141..3517a420e 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 45a2a40be0a677808a3a269311f1b5abaa3b30ba
+ revision: 9bcc244043b16ef0202f8fe93aaeb246d841f9eb
branch: offline-cache
specs:
turbo-rails (2.0.21)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index b3f59aa60..cfdf5468f 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -1,60 +1,61 @@
importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
-<% if hotwire_native_app? %>
- TurboOffline.addRule({
- match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
- handler: TurboOffline.handlers.cacheFirst({
- cacheName: "assets",
- maxAge: 60 * 60 * 24 * 7
- })
+// Documents - top-level navigation requests only
+// We need `cache: "no-cache"` here to work around
+// an annoying Safari PWA bug that predates offline mode
+TurboOffline.addRule({
+ match: (request) => request.destination === "document",
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "main",
+ maxAge: 60 * 60 * 24 * 3,
+ networkTimeout: 3,
+ fetchOptions: { cache: "no-cache" }
})
+})
- TurboOffline.addRule({
- match: /\/(boards|cards|users)\//,
- except: /\/(edit|pin|watch|new)$/,
- handler: TurboOffline.handlers.networkFirst({
- cacheName: "cards",
- maxAge: 60 * 60 * 24 * 3,
- networkTimeout: 2
- })
+TurboOffline.addRule({
+ match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
+ handler: TurboOffline.handlers.cacheFirst({
+ cacheName: "assets",
+ maxAge: 60 * 60 * 24 * 7
})
+})
- TurboOffline.addRule({
- match: /\/rails\/active_storage\//,
- handler: TurboOffline.handlers.networkFirst({
- cacheName: "storage",
- maxAge: 60 * 60 * 24 * 7,
- networkTimeout: 2
- })
+TurboOffline.addRule({
+ match: /\/(boards|cards|users)\//,
+ except: /\/(edit|pin|watch|new)$/,
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "main",
+ maxAge: 60 * 60 * 24 * 3,
+ networkTimeout: 2
})
+})
- // Everything else
- TurboOffline.addRule({
- handler: TurboOffline.handlers.networkFirst({
- cacheName: "misc",
- maxAge: 60 * 60 * 24,
- networkTimeout: 3
- })
+TurboOffline.addRule({
+ match: /\/rails\/active_storage\//,
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "storage",
+ maxAge: 60 * 60 * 24 * 7,
+ networkTimeout: 2
})
+})
- TurboOffline.start()
-<% else %>
- self.addEventListener("fetch", (event) => {
- if (event.request.method !== "GET") return
-
- if (event.request.destination === "document") {
- event.respondWith(
- fetch(event.request, { cache: "no-cache" })
- .catch(() => caches.match(event.request))
- )
- }
+// Everything else
+TurboOffline.addRule({
+ handler: TurboOffline.handlers.networkFirst({
+ cacheName: "main",
+ maxAge: 60 * 60 * 24,
+ networkTimeout: 3
})
-<% end %>
+})
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim())
})
+TurboOffline.start()
+
+
self.addEventListener("push", (event) => {
const data = event.data.json()
event.waitUntil(Promise.all([ showNotification(data), updateBadgeCount(data.options) ]))
From 90d5a351f0ef3ab712056710f17ff4df65b33ea0 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Thu, 29 Jan 2026 21:03:50 +0100
Subject: [PATCH 07/18] Bump turbo-rails for Range requests support
Cached video and audio files.
---
Gemfile.lock | 4 ++--
Gemfile.saas.lock | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 3cad12c54..405a68bac 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,10 +13,10 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 9bcc244043b16ef0202f8fe93aaeb246d841f9eb
+ revision: 9bc39c9d72f2f78ebcb30dac645eb1261a1574b3
branch: offline-cache
specs:
- turbo-rails (2.0.21)
+ turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index 3517a420e..2b9f32fd6 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,10 +62,10 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 9bcc244043b16ef0202f8fe93aaeb246d841f9eb
+ revision: 9bc39c9d72f2f78ebcb30dac645eb1261a1574b3
branch: offline-cache
specs:
- turbo-rails (2.0.21)
+ turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
From 32134be88216b0c7c6e5407171a5be88da6d4253 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Sun, 1 Feb 2026 15:15:34 +0100
Subject: [PATCH 08/18] Bump turbo-rails for network-first strategy fix on
network error
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 405a68bac..aa14545ca 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 9bc39c9d72f2f78ebcb30dac645eb1261a1574b3
+ revision: cdf9d18a99906c349e01fbe05c6c7cdaa71ce550
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index 2b9f32fd6..a4033b12b 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 9bc39c9d72f2f78ebcb30dac645eb1261a1574b3
+ revision: cdf9d18a99906c349e01fbe05c6c7cdaa71ce550
branch: offline-cache
specs:
turbo-rails (2.0.23)
From 9b264483d11123827ccf556e2eedf91601eae9fe Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Thu, 5 Feb 2026 20:23:36 +0100
Subject: [PATCH 09/18] Bump `turbo-rails` for cache size limits
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index aa14545ca..9dd682dce 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: cdf9d18a99906c349e01fbe05c6c7cdaa71ce550
+ revision: 0becdbbcdbf607eab6632463bc63f94d64249b7b
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index a4033b12b..8f5c9dd95 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: cdf9d18a99906c349e01fbe05c6c7cdaa71ce550
+ revision: 0becdbbcdbf607eab6632463bc63f94d64249b7b
branch: offline-cache
specs:
turbo-rails (2.0.23)
From b93808235c058c1f2ed422a713f4f97cf09389ab Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Thu, 5 Feb 2026 21:15:22 +0100
Subject: [PATCH 10/18] Limit cached entry sizes
1MB and no limit for number of entries except for attachments, where we
limit individual entries to 2MB and total of entries to 500. The number
is based on the following percentiles for Active Storage blobs:
```
p50: 97.1044921875 KB
p75: 236.9140625 KB
p90: 917.7548828125 KB
```
---
app/views/pwa/service_worker.js.erb | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index cfdf5468f..84f274673 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -9,7 +9,8 @@ TurboOffline.addRule({
cacheName: "main",
maxAge: 60 * 60 * 24 * 3,
networkTimeout: 3,
- fetchOptions: { cache: "no-cache" }
+ fetchOptions: { cache: "no-cache" },
+ maxEntrySize: 1024 * 1024
})
})
@@ -17,7 +18,8 @@ TurboOffline.addRule({
match: /\/assets\/.+[-\.][0-9a-f]+\.(js|css|svg|png|jpg|webp|woff2?|ico)$/,
handler: TurboOffline.handlers.cacheFirst({
cacheName: "assets",
- maxAge: 60 * 60 * 24 * 7
+ maxAge: 60 * 60 * 24 * 7,
+ maxEntrySize: 1024 * 1024
})
})
@@ -27,7 +29,8 @@ TurboOffline.addRule({
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24 * 3,
- networkTimeout: 2
+ networkTimeout: 2,
+ maxEntrySize: 1024 * 1024
})
})
@@ -36,7 +39,9 @@ TurboOffline.addRule({
handler: TurboOffline.handlers.networkFirst({
cacheName: "storage",
maxAge: 60 * 60 * 24 * 7,
- networkTimeout: 2
+ networkTimeout: 2,
+ maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
+ maxEntries: 500
})
})
@@ -45,7 +50,8 @@ TurboOffline.addRule({
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24,
- networkTimeout: 3
+ networkTimeout: 3,
+ maxEntrySize: 1024 * 1024
})
})
From 089fcf94f8969b432ba113b18a924cd85836ab05 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 6 Feb 2026 12:37:39 +0100
Subject: [PATCH 11/18] Preload resources after service worker installation
When the service worker is registered for the first time, resources loaded
before it becomes active won't go through the service worker. These resources
may be served from the browser's HTTP cache on subsequent requests, bypassing
the service worker cache entirely.
The new `preload` option accepts a regex pattern. On first visit (when no
controller exists), it waits for the service worker to take control, then
sends a message with URLs from `performance.getEntriesByType("resource")`
that match the pattern. The service worker fetches and caches these resources.
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
app/javascript/initializers/offline.js | 7 ++++---
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 9dd682dce..758936188 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 0becdbbcdbf607eab6632463bc63f94d64249b7b
+ revision: ca62e8bdad88d01914d512ad24424e0670f239c9
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index 8f5c9dd95..a3552dacd 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 0becdbbcdbf607eab6632463bc63f94d64249b7b
+ revision: ca62e8bdad88d01914d512ad24424e0670f239c9
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/app/javascript/initializers/offline.js b/app/javascript/initializers/offline.js
index c5eff824e..446a9671e 100644
--- a/app/javascript/initializers/offline.js
+++ b/app/javascript/initializers/offline.js
@@ -1,6 +1,7 @@
import { Turbo } from "@hotwired/turbo-rails"
-Turbo.offline.start("/service-worker.js", {
- scope: "/",
- native: true
+Turbo.offline.start("/service-worker.js", {
+ scope: "/",
+ native: true,
+ preload: /\/assets\//
})
From 1c2de6ca79bdd5617e5668c6cb37bf525f3c7085 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 20 Feb 2026 11:31:36 +0100
Subject: [PATCH 12/18] Clear offline cache on sign-in as well as sign-out
Rename logout controller to clear-offline-cache and attach it to the
magic link verification form so the service worker cache is cleared
when a different user signs in.
Co-Authored-By: Claude Opus 4.6
---
.../{logout_controller.js => clear_offline_cache_controller.js} | 0
app/views/my/menus/_settings.html.erb | 2 +-
app/views/sessions/magic_links/show.html.erb | 2 +-
app/views/users/show.html.erb | 2 +-
4 files changed, 3 insertions(+), 3 deletions(-)
rename app/javascript/controllers/{logout_controller.js => clear_offline_cache_controller.js} (100%)
diff --git a/app/javascript/controllers/logout_controller.js b/app/javascript/controllers/clear_offline_cache_controller.js
similarity index 100%
rename from app/javascript/controllers/logout_controller.js
rename to app/javascript/controllers/clear_offline_cache_controller.js
diff --git a/app/views/my/menus/_settings.html.erb b/app/views/my/menus/_settings.html.erb
index 6b711544b..bf34b991d 100644
--- a/app/views/my/menus/_settings.html.erb
+++ b/app/views/my/menus/_settings.html.erb
@@ -6,7 +6,7 @@
<%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %>
<%= icon_tag "logout", class: "popup__icon" %>
- <%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", form: { data: { turbo: false, controller: "logout", action: "submit->logout#clearCache" } } do %>
+ <%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", form: { data: { turbo: false, controller: "clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do %>
Sign out
<% end %>
<% end %>
diff --git a/app/views/sessions/magic_links/show.html.erb b/app/views/sessions/magic_links/show.html.erb
index aba8411f5..968624a10 100644
--- a/app/views/sessions/magic_links/show.html.erb
+++ b/app/views/sessions/magic_links/show.html.erb
@@ -8,7 +8,7 @@
- <%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
+ <%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do |form| %>
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large txt-uppercase",
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index a285a4f5a..eb3e40c64 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -41,7 +41,7 @@
<% if Current.user == @user %>
- <%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", form: { data: { turbo: false, controller: "logout", action: "submit->logout#clearCache" } } do %>
+ <%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", form: { data: { turbo: false, controller: "clear-offline-cache", action: "submit->clear-offline-cache#clearCache" } } do %>
Sign out of Fizzy on this device
<% end %>
<% end %>
From de6d70cc2efa1aa3f972d2bb1427a1731c9c2710 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 20 Feb 2026 11:45:06 +0100
Subject: [PATCH 13/18] Bump turbo-rails for Turbo.offline.clearCache()
Simplify the clear-offline-cache controller to use the new
Turbo.offline.clearCache() API instead of messaging the service
worker directly.
Co-Authored-By: Claude Opus 4.6
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
.../controllers/clear_offline_cache_controller.js | 6 +++---
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 758936188..8a49b38a5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: ca62e8bdad88d01914d512ad24424e0670f239c9
+ revision: 4104a353cfe728232dc0eed6d54c77466a9849f2
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index a3552dacd..751dc55fb 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: ca62e8bdad88d01914d512ad24424e0670f239c9
+ revision: 4104a353cfe728232dc0eed6d54c77466a9849f2
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/app/javascript/controllers/clear_offline_cache_controller.js b/app/javascript/controllers/clear_offline_cache_controller.js
index bc0511a7f..7caa2e89b 100644
--- a/app/javascript/controllers/clear_offline_cache_controller.js
+++ b/app/javascript/controllers/clear_offline_cache_controller.js
@@ -1,8 +1,8 @@
import { Controller } from "@hotwired/stimulus"
+import { Turbo } from "@hotwired/turbo-rails"
export default class extends Controller {
- async clearCache() {
- const registration = await navigator.serviceWorker?.ready
- registration?.active?.postMessage({ action: "clearCache" })
+ clearCache() {
+ Turbo.offline.clearCache()
}
}
From 0482670fec187f3361531a6b8a4672430b65345f Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 20 Feb 2026 12:07:38 +0100
Subject: [PATCH 14/18] Exclude service worker and edit/pin/watch/new pages
from main cache
We don't need the service worker to cache itself, or pages that won't
work offline anyway.
---
app/javascript/controllers/magic_link_controller.js | 2 +-
app/views/pwa/service_worker.js.erb | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/javascript/controllers/magic_link_controller.js b/app/javascript/controllers/magic_link_controller.js
index 3e2e4618c..8217af5aa 100644
--- a/app/javascript/controllers/magic_link_controller.js
+++ b/app/javascript/controllers/magic_link_controller.js
@@ -15,7 +15,7 @@ export default class extends Controller {
submit() {
if (this.inputTarget.disabled) return
- this.element.submit()
+ this.element.requestSubmit()
this.inputTarget.disabled = true
}
}
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index 84f274673..095595f4e 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -5,6 +5,7 @@ importScripts("<%= javascript_url("turbo-offline-umd.min") %>")
// an annoying Safari PWA bug that predates offline mode
TurboOffline.addRule({
match: (request) => request.destination === "document",
+ except: /\/(edit|pin|watch|new)$/,
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24 * 3,
@@ -47,6 +48,7 @@ TurboOffline.addRule({
// Everything else
TurboOffline.addRule({
+ except: /\/(service-worker\.js|edit|pin|watch|new)$/,
handler: TurboOffline.handlers.networkFirst({
cacheName: "main",
maxAge: 60 * 60 * 24,
From 99382db7203f1598c27d71b612a75230cc8b83b6 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 20 Feb 2026 12:43:18 +0100
Subject: [PATCH 15/18] Only start offline mode when signed in
Gate Turbo.offline.start() behind Current.user so the service worker
is only registered for authenticated users. This avoids errors on
unauthenticated pages where /service-worker.js requires auth.
Co-Authored-By: Claude Opus 4.6
---
app/javascript/initializers/offline.js | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/app/javascript/initializers/offline.js b/app/javascript/initializers/offline.js
index 446a9671e..877ffa0ef 100644
--- a/app/javascript/initializers/offline.js
+++ b/app/javascript/initializers/offline.js
@@ -1,7 +1,9 @@
import { Turbo } from "@hotwired/turbo-rails"
-Turbo.offline.start("/service-worker.js", {
- scope: "/",
- native: true,
- preload: /\/assets\//
-})
+if (Current.user) {
+ Turbo.offline.start("/service-worker.js", {
+ scope: "/",
+ native: true,
+ preload: /\/assets\//
+ })
+}
From 67d629f72219b72936127bd12ae7721c4e7a33bb Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Fri, 20 Feb 2026 20:48:01 +0100
Subject: [PATCH 16/18] Use CORS fetch mode for Active Storage to enable
`maxEntrySize` checks
Otherwise, for resources like images loaded via
tags, the browser
sets `mode: "no-cors"` (as these aren't CORS requests), so the service
worker gets an opaque response even though the server sends CORS
headers.
We could upgrade all no-cors requests to cors mode, sending a `mode:
"cors"` request to a server that doesn't send CORS headers will fail
entirely: the `fetch` call throws a `TypeError` (network error), and the
browser blocks the response. So the resource wouldn't load at all, not
even as opaque. We wouldn't be able to cache it at all.
By opting in via `fetchOptions`, we can do it only for rules where we
know the server will send CORS headers.
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
app/views/pwa/service_worker.js.erb | 3 ++-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 8a49b38a5..e811a1c2f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 4104a353cfe728232dc0eed6d54c77466a9849f2
+ revision: 43bc205155b0eb57e5e016ed0e87a12c29ba2ab2
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index 751dc55fb..af3c102c1 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 4104a353cfe728232dc0eed6d54c77466a9849f2
+ revision: 43bc205155b0eb57e5e016ed0e87a12c29ba2ab2
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index 095595f4e..5ab493e22 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -42,7 +42,8 @@ TurboOffline.addRule({
maxAge: 60 * 60 * 24 * 7,
networkTimeout: 2,
maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
- maxEntries: 500
+ maxEntries: 500,
+ fetchOptions: { mode: "cors", credentials: "omit" }
})
})
From 1855490f805477db7bd0eebdca3fc52da1a8f0c7 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Sat, 21 Feb 2026 09:20:47 +0100
Subject: [PATCH 17/18] Revert CORS fetch mode for Active Storage
CORS mode doesn't work with redirect-based Active Storage URLs
when the storage bucket uses wildcard Access-Control-Allow-Origin.
Relying on maxEntries alone until specific origin CORS is available.
Co-Authored-By: Claude Opus 4.6
---
Gemfile.lock | 2 +-
Gemfile.saas.lock | 2 +-
app/views/pwa/service_worker.js.erb | 3 +--
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index e811a1c2f..8db6817dc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,7 +13,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 43bc205155b0eb57e5e016ed0e87a12c29ba2ab2
+ revision: 93ce5bc461cd71af1797e79fcd7e09a19242d080
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index af3c102c1..37ff08294 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -62,7 +62,7 @@ GIT
GIT
remote: https://github.com/hotwired/turbo-rails.git
- revision: 43bc205155b0eb57e5e016ed0e87a12c29ba2ab2
+ revision: 93ce5bc461cd71af1797e79fcd7e09a19242d080
branch: offline-cache
specs:
turbo-rails (2.0.23)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index 5ab493e22..095595f4e 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -42,8 +42,7 @@ TurboOffline.addRule({
maxAge: 60 * 60 * 24 * 7,
networkTimeout: 2,
maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
- maxEntries: 500,
- fetchOptions: { mode: "cors", credentials: "omit" }
+ maxEntries: 500
})
})
From 49a86c7cbf7905d7cb67e1e69e2922040658a279 Mon Sep 17 00:00:00 2001
From: Rosa Gutierrez
Date: Tue, 3 Mar 2026 15:59:25 +0000
Subject: [PATCH 18/18] Enable CORS fetch mode for storage requests
The load balancer now returns a specific `Access-Control-Allow-Origin`
instead of a wildcard, so credentials can be included by default.
This enables `maxEntrySize` enforcement for storage.
Co-Authored-By: Claude Opus 4.6
---
app/views/pwa/service_worker.js.erb | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/views/pwa/service_worker.js.erb b/app/views/pwa/service_worker.js.erb
index 095595f4e..41fac1446 100644
--- a/app/views/pwa/service_worker.js.erb
+++ b/app/views/pwa/service_worker.js.erb
@@ -42,7 +42,8 @@ TurboOffline.addRule({
maxAge: 60 * 60 * 24 * 7,
networkTimeout: 2,
maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
- maxEntries: 500
+ maxEntries: 500,
+ fetchOptions: { mode: "cors" }
})
})