<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdulfetah Suudi</title>
    <description>The latest articles on DEV Community by Abdulfetah Suudi (@iamsuudi).</description>
    <link>https://dev.to/iamsuudi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1371001%2F826d28e8-7097-422b-9257-5c73ddbdbf54.jpeg</url>
      <title>DEV Community: Abdulfetah Suudi</title>
      <link>https://dev.to/iamsuudi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iamsuudi"/>
    <language>en</language>
    <item>
      <title>How to Add Offline Support to Your Web App (without hand-writing a service worker)</title>
      <dc:creator>Abdulfetah Suudi</dc:creator>
      <pubDate>Sat, 12 Sep 2026 08:41:48 +0000</pubDate>
      <link>https://dev.to/iamsuudi/how-to-add-offline-support-to-your-web-app-without-hand-writing-a-service-worker-5gki</link>
      <guid>https://dev.to/iamsuudi/how-to-add-offline-support-to-your-web-app-without-hand-writing-a-service-worker-5gki</guid>
      <description>&lt;p&gt;&lt;strong&gt;The full walkthrough:&lt;/strong&gt; one config file, an auditable vanilla-JS service worker + client runtime generated for you, zero runtime dependencies — and a live demo you can test offline right now.&lt;/p&gt;




&lt;p&gt;Every serious web app eventually needs to work when the network doesn't. And every time, the work is the same: hand-writing a service worker, wired caching strategies, an offline fallback page, and — before long — auth handling and a queue for mutations that happened while offline.&lt;/p&gt;

&lt;p&gt;A while back I got tired of re-writing that boilerplate for every project, in every framework. So I built &lt;strong&gt;Swoff&lt;/strong&gt;: a config-driven generator that writes the service worker and client runtime into your repo as plain, auditable vanilla JavaScript. No runtime library injected into your bundle. MIT-licensed. &lt;strong&gt;If your stack has a &lt;code&gt;fetch&lt;/code&gt; event, it works.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post I'll walk through the whole thing: init, generate, build step, inject — and show you a working offline-first site to test against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-command promise
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @swoff/cli init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx @swoff/cli generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. The CLI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;init&lt;/code&gt;&lt;/strong&gt; — interactive wizard (framework auto-detected, output directory, navigation mode, caching strategy, features), or &lt;code&gt;--yes&lt;/code&gt; for defaults. Writes &lt;code&gt;swoff.config.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;generate&lt;/code&gt;&lt;/strong&gt; — writes the &lt;code&gt;swoff/&lt;/code&gt; runtime: the service-worker template, the generator, the client injector (ESM + IIFE), storage/reset utilities, and TypeScript declarations. Feature-specific files appear only when you enable the feature.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A real &lt;code&gt;swoff.config.json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's what a typical config looks like (truncated to the interesting parts):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"swOutput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"swoffPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/swoff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"swUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/swoff.sw.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"navigation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/offline"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"features"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"caching"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cache-first"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"patterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"/api/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stale-while-revalidate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"/api/checkout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"network-first"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mutationQueue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pushNotifications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pwa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single config drives a lot. One note on strategy naming — Swoff supports &lt;strong&gt;6 caching strategies&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cache-first&lt;/code&gt; — serve cache, update in the background&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;network-first&lt;/code&gt; — try network, fall back to cache on failure (great for navigation)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stale-while-revalidate&lt;/code&gt; — serve stale instantly, refresh from network&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache-only&lt;/code&gt; / &lt;code&gt;network-only&lt;/code&gt; — the two extremes, when you want full control&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reactive&lt;/code&gt; — serve cache as "fresh" for &lt;code&gt;staleTime&lt;/code&gt; seconds, then serve cached &lt;em&gt;and&lt;/em&gt; trigger a background refresh; add a &lt;code&gt;refetchInterval&lt;/code&gt; for periodic background refetch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strategies resolve in 3 tiers (per-request header → per-route &lt;code&gt;patterns&lt;/code&gt; → &lt;code&gt;default&lt;/code&gt;), so you can pin one hot route and leave everything else on a sane default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the build step
&lt;/h2&gt;

&lt;p&gt;The generator embeds your latest assets into the service worker, so it must run after every build. Simplest version — append to your &lt;code&gt;build&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build &amp;amp;&amp;amp; node swoff/sw/generator.mjs"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Or run &lt;code&gt;node swoff/sw/generator.mjs&lt;/code&gt; manually whenever you ship.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Including the injector (bundler or not)
&lt;/h2&gt;

&lt;p&gt;This is where Swoff really differs from framework-locked plugins — pick your shape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundler frameworks&lt;/strong&gt; (React, Vue, Svelte, HTMX, Solid; Next.js, Nuxt, SvelteKit, TanStack Start, Astro):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initServiceWorker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./swoff/client-injector&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;initServiceWorker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No-bundler projects&lt;/strong&gt; (Go, Laravel, Rails, Django, Flask, plain HTML/JS — no Node runtime needed in prod):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/swoff/client-injector.bundle.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IIFE bundle self-initializes, so a plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag is all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying it works
&lt;/h2&gt;

&lt;p&gt;Open your deployed site, then &lt;strong&gt;DevTools → Application → Service Workers&lt;/strong&gt;. You should see the Swoff SW &lt;em&gt;activated and running&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Head to &lt;strong&gt;Cache Storage&lt;/strong&gt; and you'll find structured caches (e.g. &lt;code&gt;swoff-precache&lt;/code&gt;, &lt;code&gt;swoff-runtime&lt;/code&gt;, &lt;code&gt;swoff-runtime-html&lt;/code&gt;). Finally, switch DevTools &lt;strong&gt;Network&lt;/strong&gt; to &lt;strong&gt;Offline&lt;/strong&gt; and reload — the site serves from the precache, with navigation falling back to your offline route when a page isn't cached yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The living demo
&lt;/h2&gt;

&lt;p&gt;The best part is that &lt;strong&gt;this toolkit dogfoods itself&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://swoff.space" rel="noopener noreferrer"&gt;https://swoff.space&lt;/a&gt;&lt;/strong&gt; — the Swoff docs site — runs offline-first on Swoff. Its service worker is generated by the tool and precaches the entire site including an &lt;code&gt;/offline&lt;/code&gt; route. Open it, throttle your network to something unflattering (or go fully offline), and the whole site keeps working. Refresh. Navigate. Read.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a screenshot. It's the running product.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else runs on this (feature surface)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline mutation queue&lt;/strong&gt; — queue writes while offline, flush via background sync when you're back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt; — token storage, 401 interception, refresh-before-request, offline auth state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL&lt;/strong&gt; — caching + server push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notifications&lt;/strong&gt; — subscribe/unsubscribe, notification handling, cleanup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PWA install&lt;/strong&gt; — installability, splash, theme&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag-based invalidation&lt;/strong&gt; — evict cached assets by tag instead of nuking the whole cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it generated as plain code you can read, edit, and debug. &lt;strong&gt;You own every line.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @swoff/cli init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx @swoff/cli generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs (the running demo):&lt;/strong&gt; &lt;a href="https://swoff.space" rel="noopener noreferrer"&gt;https://swoff.space&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@swoff/cli" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@swoff/cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/iamsuudi/swoff" rel="noopener noreferrer"&gt;https://github.com/iamsuudi/swoff&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built offline-first apps by hand — or been burned by a hand-rolled service worker in production — I'd genuinely love to hear how your approach differs, and where Swoff falls short.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Bonus: the same repo also ships &lt;a href="https://www.npmjs.com/package/@swoff/assets" rel="noopener noreferrer"&gt;&lt;code&gt;@swoff/assets&lt;/code&gt;&lt;/a&gt; — 50+ PWA assets (icons, adaptive, splash, head tags) from one image or a wordmark. Worth a look if the asset grind is your pain point.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>javascript</category>
      <category>offline</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
