Skip to main content

05 — PWA best practices (2025–2026) and embedding strategy for Rasd Forms

Research date: 2026-08-15. Scope: how an embeddable React / React Native forms library should behave inside a customer's PWA (service worker, caching, sync, install, iOS constraints) and how it should be shipped so a customer can "add it to their website or app" (npm, script tag/CDN, Web Component, iframe), without the library owning the customer's service worker.

Summary

  1. Workbox is alive but in maintenance mode: workbox-* 7.4.1 shipped 2026‑05‑04 (MIT); the 7.1→7.4 releases are dependency/security bumps only, no new features [2][4]. Build on it, but keep our own abstractions thin.
  2. Framework integrations to target: vite-plugin-pwa 1.3.0 (2026‑05‑05, MIT, supports Vite 3–8, pins workbox-build/workbox-window ^7.4.1) [3]; serwist / @serwist/next 9.5.12 (2026‑07‑22, MIT) plus @serwist/turbopack for Next 15/16 [5][23][24]. next-pwa (5.6.0, last publish 2022‑08‑23; repo archived Aug 2023) and its fork @ducanh2912/next-pwa (10.2.9, 2024‑09‑18) are dead/dormant — do not document them [4].
  3. Chrome no longer requires a service worker to install a PWA (Chrome 108 mobile / 112 desktop) and Lighthouse removed its PWA category in Lighthouse 12 (PSI switched 2024‑05‑10) [12][13][22]. "PWA score" is not a marketing metric any more; installability = HTTPS + manifest (name/short_name, 192 & 512 px icons, start_url, display) [14].
  4. iOS 26 (Sep 2025): every site added to the Home Screen opens as a web app by default; a manifest is no longer required, SVG icons are supported [16]. Home‑screen web apps get browser‑level storage quota (up to 60 % of disk per origin) and are exempt from the ITP 7‑day script‑storage purge; the purge still applies to forms filled in a normal Safari tab [7][8][9].
  5. Background Sync API is Chromium‑only (Chrome 49+, Edge 79+, Samsung Internet 5+; not Safari through 26.5, not Firefox through 153; 76.7 % global usage) and Periodic Background Sync requires an installed Chromium PWA with a positive site‑engagement score [10][11]. Rasd's sync engine must be an IndexedDB outbox that works without either API; sync is an optional accelerator.
  6. Web Push works on iOS only for Home Screen web apps (16.4+); iOS 18.4 / macOS 15.5 added Declarative Web Push ({"web_push": 8030, "notification": {...}}), which needs no service worker and is backward compatible with standard Web Push [17]. Push is optional for Rasd; if used, send declarative payloads.
  7. Update UX: Workbox's recommended pattern is waiting → prompt → messageSkipWaiting()controlling → reload; workbox-build defaults are skipWaiting:false, clientsClaim:false, cleanupOutdatedCaches:false, maximumFileSizeToCacheInBytes: 2 097 152 [19][20][21]. A forms library must never trigger a reload while a draft is being edited.
  8. Cross‑origin iframes are the wrong vehicle for offline‑first: Chrome 115+ partitions IndexedDB, Cache Storage and SW registrations by top‑level site; Safari gives cross‑origin frames only 10 % of the parent origin's quota [7][9][38]. Typeform and Tally embed via iframe (online SaaS), while SurveyJS, Form.io and Formbricks render in the host DOM (npm + CDN script) [25][27][28][29][30].
  9. Shadow DOM is now practical for React widgets: React 19 passes Custom Elements Everywhere; the proven recipe is attachShadow({mode:'open'}) + createRoot(shadowRoot) + stylesheet injected into the shadow root (SurveyJS ships this exact demo; CSS‑in‑JS added 150–200 ms in one production write‑up) [26][31][32].
  10. A library should not own the customer's SW. The established third‑party patterns are: (a) importScripts()‑able worker bundle + exported route/plugin factories for Workbox/Serwist users (OneSignal, Firebase), (b) a scoped, separately registered helper SW when the app has none (OneSignal recommends separate scopes), (c) a CLI that copies a static worker into public/ (MSW npx msw init) [35][36][37]. Rasd should ship all three, defaulting to (a).

1. Service‑worker toolchain status (verified from npm registry, 2026‑08‑15)

PackageLatestPublishedLicenceNotes
workbox-build, workbox-window, workbox-core, workbox-background-sync7.4.12026‑05‑04MIT7.3/7.4 = "critical dependency updates"; 7.4.1 migrated to Rollup 4, replaced lodash [2][4]
vite-plugin-pwa1.3.02026‑05‑05MITpeers vite ^3–^8, workbox-build ^7.4.1; strategies generateSW / injectManifest; `registerType: 'prompt'
serwist, @serwist/next9.5.122026‑07‑22MITWorkbox‑derived, TypeScript‑first; Serwist class options precacheEntries, skipWaiting, clientsClaim, navigationPreload, runtimeCaching, fallbacks; @serwist/turbopack builds sw.ts from a route handler app/serwist/[path]/route.ts with esbuild; injection point self.__SW_MANIFEST [5][23][24]
next-pwa5.6.02022‑08‑23MITRepo archived 2023‑08‑18; requires webpack (next build --webpack on Next 16) [4][40]
@ducanh2912/next-pwa10.2.92024‑09‑18MITFork; effectively superseded by Serwist [4]
@vite-pwa/nuxt / @vite-pwa/remix1.1.1 / 0.2.02026‑02‑06 / 2025‑03‑30MITSame core as vite-plugin-pwa [4]
Expo webSDK 53/54docs updated 2026‑06‑03Metro has no first‑class SW support; official recipe is expo export -p webdist/ then npx workbox-cli generateSW workbox-config.js as a post‑build step; manifest at public/manifest.json; Expo explicitly warns SWs "are known to cause unexpected behavior on web" [34]

Implication: Rasd must integrate with customer‑owned Workbox 7 / Serwist 9 workers, and with the "no bundler" case (workbox‑cli generateSW + importScripts), because Expo web customers will be in that case.

2. Caching design: precache vs runtime, budgets, expiration, versioning

  • Precache only the app shell the customer builds; the library must not add entries to the customer's precache manifest. workbox-build defaults: globPatterns ['**/*.{js,wasm,css,html}'], maximumFileSizeToCacheInBytes 2 MB (CRA raised its template to 5 MB after hitting the limit on lazy chunks; Excalidraw hit the same wall) [21][39]. Rasd's web runtime therefore should stay comfortably below 2 MB uncompressed per chunk, and the builder (drag‑and‑drop) must be a separate lazy chunk so field‑runner PWAs never precache it.
  • Runtime caching is where a forms library legitimately contributes: form definitions (JSON), choice lists, question media, i18n bundles, fonts (Arabic). Workbox strategies + plugins: NetworkFirst/StaleWhileRevalidate for definitions, CacheFirst + ExpirationPlugin({maxEntries, maxAgeSeconds}) + CacheableResponsePlugin({statuses:[0,200]}) for media; runtimeCaching[].options.expiration and .backgroundSync are first‑class in generateSW config [1][21].
  • Cache versioning: precache entries carry revisions; enable cleanupOutdatedCaches() (default off) so old precaches are removed on activate; the plain‑JS pattern is a CACHE_VERSION constant that busts on install [18][20][21]. Rasd runtime caches should be namespaced (rasd-defs-v1, rasd-media-v1) and versioned independently of the customer's precache so a library upgrade can migrate its own caches without touching theirs.
  • Offline fallback: precache a self‑contained offline.html (inline CSS/JS); handle request.mode === 'navigate' with navigation preload → network → fallback; note that 4xx/5xx responses do not hit catch(), only network errors do [18]. Serwist exposes fallbacks: { entries: [{ url: '/~offline', matcher }] } [5]. Rasd should provide an optional offline component (form list from IndexedDB) rather than a fallback page — the SPA shell is the customer's.

3. Background Sync, Periodic Sync and what a sync engine may assume

  • workbox-background-sync BackgroundSyncPlugin(queueName, {maxRetentionTime}) hooks fetchDidFail; it queues only on network exceptions, not on 4xx/5xx; browsers without the API replay "whenever your service worker starts up" (i.e., needs an open page) [1].
  • Support: Chrome 49+, Edge 79+, Opera 42+, Samsung Internet 5+; no Safari (through 26.5), no Firefox (through 153) [10]. Periodic Background Sync: Chromium only, installed PWA only, frequency governed by site‑engagement score, only on previously‑joined networks, minInterval is a floor not a schedule [11].
  • Consequence for enumerator apps on low‑end Android (Chrome/Samsung Internet dominate) and on iPhones: sync must be an app‑level outbox in IndexedDB with triggers on online, visibilitychange, app start, timer, and manual "Sync now"; when 'sync' in registration exists, register a tag as an accelerator; when periodicSync exists and permission periodic-background-sync is granted, refresh form definitions opportunistically [10][11]. This is exactly why the SW cannot be the source of truth for the queue.

4. Update UX (skipWaiting vs prompt)

Workbox's guidance: register with new Workbox('/sw.js'), listen for waiting, show a prompt, call wb.messageSkipWaiting(), then on controlling call location.reload(); the SW listens for {type:'SKIP_WAITING'}. Blind skipWaiting() is discouraged because lazily loaded, hash‑versioned chunks may no longer exist in the new precache [19][20]. vite‑plugin‑pwa exposes this as registerType: 'prompt' (default) vs 'autoUpdate' (which forces skipWaiting+clientsClaim and reloads) [3][6]. Workbox events expose isUpdate / isExternal flags so a UI can distinguish first install from update [20].

For a forms library the extra rule is transactional safety: an update‑triggered reload while an enumerator is mid‑form is a data‑loss event. Rasd should (a) autosave drafts to IndexedDB on every change and (b) export a useRasdBusy() / rasd.isDirty() signal so the host's update prompt can defer reload; the library must never call skipWaiting, clientsClaim or location.reload() itself.

5. Manifest, install prompts, iOS behaviour

  • Chromium install criteria (web.dev, updated 2024‑09‑19): HTTPS; manifest with name/short_name, icons including 192 px and 512 px, start_url, display ∈ {fullscreen, standalone, minimal‑ui, window‑controls‑overlay}, prefer_related_applications absent/false; user has clicked once and spent ≥30 s on the site; not already installed [14]. A SW fetch handler is no longer required for menu installation (Chrome 108 Android / 112 desktop); Chrome recommends maskable icons; screenshots + description enable the richer install sheet [12][13][14].
  • beforeinstallprompt (Chromium only): preventDefault(), keep the event, show your own button, prompt() once, read userChoice; listen to appinstalled; detect standalone with matchMedia('(display-mode: standalone)') plus navigator.standalone on iOS [15]. iOS has no beforeinstallprompt; installation is Share → Add to Home Screen, and since iOS 26 (Sept 2025) the "Open as Web App" toggle is on by default for any site, manifest or not [16]. Rasd should ship a small <InstallHint> that renders Chromium's prompt button or iOS instructions (RTL‑aware, Arabic strings), because enumerators must be installed to get durable storage (next section).

6. iOS / Safari constraints that matter for field data

ConstraintFact (primary source)Impact on Rasd
Storage quota (Safari 17+, iOS 17+)Origin quota up to 60 % of disk in browser apps / 15 % in other apps' WebViews; overall 80 % / 20 %; cross‑origin frames get 10 % of the main frame's origin quota; Home‑Screen web apps get browser quotas [7][9]Fine for surveys; iframes are the weak spot
7‑day script‑storage purge (ITP, Safari 13.1+)IndexedDB, localStorage, Cache and SW registrations deleted after 7 days of Safari use without interaction; Home‑Screen web apps have their own counter and are not expected to lose data [8][9]Non‑installed Safari users can lose queued submissions; push users to install; sync aggressively
navigator.storage.persist()Safari 15.2+; granted by heuristic (e.g., opened as Home‑Screen web app), Chrome grants silently by engagement/installed/notification permission, Firefox prompts [7][9][17]Call it after first successful sync/install; surface persisted() in a diagnostics panel
Web PushiOS 16.4+ Home‑Screen web apps only; Declarative Web Push on iOS 18.4/macOS 15.5, SW optional, standard Web Push still works [17]Optional feature; use declarative JSON payload for both
Background Sync / Periodic SyncNot supported in Safari [10][11]Outbox + foreground triggers (section 3)
Cache limitsOlder "50 MB cache" figures circulating in blogs predate the Safari 17 policy; treat as obsolete unless re‑verified [7]Do not hard‑code

Chrome/Android quotas for comparison: up to 60 % of disk per origin, 80 % overall, LRU eviction under pressure unless persistent; localStorage is 5 MiB per origin (never use it for submissions) [9].

7. Storage partitioning and why iframes are online‑only

Chrome 115+ partitions IndexedDB, Cache Storage, localStorage, OPFS, Broadcast Channel, SharedWorker and service worker registrations created in third‑party iframes by (origin, top‑level site) [38]. Safari applies similar partitioning plus the 10 % frame quota [7]. So a <iframe src="https://forms.rasd.io/..."> embed cannot share offline data with the host, cannot register a useful SW for the host page, and gets a fraction of the quota — acceptable for a hosted, online "share link" product, unacceptable as the primary embed for offline field work.

8. Lighthouse and how to prove "PWA quality" instead

Lighthouse removed the PWA category (issue #15535; PageSpeed Insights moved to Lighthouse 12 on 2024‑05‑10) because it tracked Chrome's installability criteria, which changed [12][22]. Recommendation: replace "Lighthouse PWA score" in marketing with a checklist (installable manifest, offline shell, persisted storage, sync queue health, update prompt) plus a rasd-forms doctor dev command and DevTools Application‑panel screenshots.

9. Embedding models compared

ModelCSS isolationSame‑origin storage & SWSSR/RSCReact duplicationCSP frictionUsed by
npm ESM package (React/RN peer deps)none unless Shadow DOM opt‑inyesneeds 'use client' + browser guardsnonelowest (self‑hosted, hashed assets)SurveyJS, Form.io, Formbricks [25][28][29]
Script tag / CDN IIFE (<script src=… data-…>)via Shadow DOM inside the bundleyesn/abundles its own React (~45 KB gz widget in one guide) [32]needs script-src allow‑list or self‑host + SRISurveyJS (unpkg), Form.io (cdn.form.io), Formbricks (/api/packages/website), Typeform (embed.typeform.com) [25][27][28][30]
Web Component (<rasd-form> custom element + Shadow DOM)strong (styles cannot leak in/out; theme via CSS custom properties)yesReact 19 renders custom elements incl. SSR attribute serialisation [31]same as IIFE unless externalisedstyle injection must be CSSOM/<link> not inline <style> under strict CSPNot yet mainstream among form vendors; SurveyJS documents a Shadow‑DOM render recipe [26]
iframetotalpartitioned; 10 % quota on Safari; no host SW [7][38]trivialnoneneeds frame-src; vendor CSP blocks insecure parents [27]Typeform, Tally [27][30]

How the reference products embed (verified 2026‑08‑15):

  • Typeform@typeform/embed (npm) or //embed.typeform.com/next/embed.js + CSS; declarative data-tf-widget="<id>" attributes; createWidget/createPopup/createSlider/createSidetab/createPopover; forms render inside iframes (iframeProps); vendor CSP blocks embedding on non‑HTTPS pages [27].
  • Tallyhttps://tally.so/widgets/embed.js, <iframe data-tally-src=…>, Tally.loadEmbeds(), Tally.openPopup()/closePopup(), events Tally.FormLoaded/FormPageView/FormSubmitted/PopupClosed; iframe with dynamic‑height messaging [30].
  • SurveyJSsurvey-core + survey-js-ui (or survey-react-ui 3.0.0, 2026‑08‑11, MIT) from npm or unpkg; new Survey.Model(json); survey.render(el); theming via CSS variables and, in v3.0, a shared design‑token system with Bootstrap/MUI/shadcn adapters; documented Shadow‑DOM recipe = append survey-core.css inside the shadow root [25][26][41].
  • Form.io@formio/js 5.5.1 (2026‑08‑11, MIT); https://cdn.form.io/js/formio.full.min.js + .css; Formio.createForm(el, url); one‑line embed formio.embed.min.js?src=…&libs=true; "Inherit Page CSS" toggle (no Shadow DOM) [28].
  • Formbricks@formbricks/js 5.0.0 (2026‑05‑27, MIT); formbricks.setup({workspaceId, appUrl}) (environmentId deprecated) or a loader script that injects ${appUrl}/api/packages/website and calls window.formbricks.init; surveys render in the host DOM as an overlay [29] (rendering internals not re‑verified today).

Web Component mechanics that are now settled: React 19 maps primitive props to attributes and object/function props to properties, and listens to custom events without refs [31]; the widget recipe is attachShadow({mode:'open'}), createRoot(shadowRoot) (or hydrateRoot to avoid a first‑paint flicker), CSS injected via <link>/constructed stylesheet inside the shadow root, config from document.currentScript.dataset, IIFE output via Vite/Rollup library mode with react/react-dom either bundled (script‑tag build) or external + output.globals (UMD build) [32][33]. Vite lib mode formats are es | cjs | umd | iife, CSS is emitted as a separate file (build.lib.cssFileName) that must be listed in exports [33]. Known Shadow‑DOM pain points to design for: portals (date pickers, dropdown menus must portal inside the shadow root or they lose styles), document.activeElement retargeting, and form‑associated custom elements if the host wants native <form> submission.

10. Exposing SW helpers without owning the customer's service worker

Third‑party precedents:

  • OneSignal: customers add importScripts("https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.sw.js") to their own SW, or register OneSignal's worker at a separate scope (serviceWorkerPath: "push/onesignal/OneSignalSDKWorker.js", serviceWorkerParam: {scope: "/push/onesignal/"}); OneSignal explicitly says separate scopes are simpler and warns that once merged, the old file must be kept for ~a year while clients re‑register [35]. Workbox users add exclude: [/OneSignal.*\.js$/] so the vendor worker is not precached [35].
  • Firebase Cloud Messaging: default firebase-messaging-sw.js at root or reuse an existing SW via getToken({serviceWorkerRegistration}) and import … from 'firebase/messaging/sw' inside a bundled worker; the compat CDN path uses importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js') [36].
  • MSW: npx msw init <publicDir> copies a static mockServiceWorker.js into the app so it is served same‑origin at /; worker.start() must be awaited [37].
  • Workbox generateSW has an importScripts option precisely for "additional JavaScript files … within the service worker" [21]; vite‑plugin‑pwa injectManifest and Serwist let the customer author sw.ts and compose registerRoute(...), plugins and runtimeCaching arrays [5][6].

Design consequences: the SW spec requires the worker script to be same‑origin (CSP worker-src, falling back to child-srcscript-srcdefault-src, blocks navigator.serviceWorker.register("https://not-example.com/sw.js")) [42]; therefore a CDN‑hosted "Rasd service worker" is impossible, and importScripts() from a CDN inside the customer's worker is subject to the CSP delivered with the worker script — customers with strict CSP will need a self‑hosted copy. Ship the worker code as an npm asset they can copy/bundle, not as a hosted URL.

11. SSR / Next.js / RSC and Expo web

  • Put 'use client' at the top of every component/hook entry that touches state, effects, DOM or IndexedDB; keep it in built output (bundlers treat it as the boundary); pure schema/validation modules stay directive‑free so they can run in Server Components and Node [33]. Client components are still SSR‑rendered, so IndexedDB/window/navigator.serviceWorker access belongs in effects or lazily created adapters; document next/dynamic(() => import('@rasd/forms-react'), { ssr:false }) for the builder.
  • Next 16 defaults to Turbopack; document Serwist (@serwist/next for webpack, @serwist/turbopack route‑handler build) rather than next-pwa [23][24].
  • Expo web (Metro): no SW; provide a workbox-config.js recipe with importScripts: ['rasd-sw.js'] and runtimeCaching entries generated by @rasd/forms-pwa/workbox-config [34]. react-native-web shares the same JS runtime, so the IndexedDB adapter is reused; native uses SQLite (out of scope here).

12. CSP checklist for an embeddable form runtime

connect-src for the customer's API + Rasd licence endpoint; worker-src 'self' (SW is always same‑origin) [42]; img-src/media-src for question media incl. blob: for offline photos; font-src for Arabic web fonts (self‑host, do not fetch Google Fonts at runtime); style-src — prefer constructed stylesheets/<link> over injected inline <style> so strict CSPs without 'unsafe-inline' still work (verify per customer); script-src — npm build needs nothing extra, CDN build needs the CDN host or a nonce with 'strict-dynamic'; publish SRI hashes for every CDN release; never require eval (no runtime expression new Function — use a sandboxed expression parser for form logic).


Implications & recommendations for Rasd Forms

  1. Distribution tiers: (a) primary — npm ESM/CJS packages @rasd/forms-core (schema, logic, validation, i18n; no DOM), @rasd/forms-react ('use client' components), @rasd/forms-native, @rasd/forms-builder (lazy‑loaded), @rasd/forms-pwa (storage adapters + SW helpers), @rasd/forms-element (Web Component); (b) secondary — a CDN/self‑hostable IIFE rasd-forms.iife.js that defines <rasd-form> and bundles React; (c) later — hosted "share‑link" iframe embed for online respondents only.
  2. Web Component with open Shadow DOM as the framework‑agnostic embed; expose theming through CSS custom properties (--rasd-*) that inherit through the shadow boundary; render popovers/date pickers into a portal container inside the shadow root; support dir="rtl" and lang attributes; keep React apps on the plain React components (no Shadow DOM) by default with an opt‑in <RasdIsolatedForm> wrapper.
  3. Never register, update or reload a SW from the library. @rasd/forms-pwa exports: rasdRuntimeCaching(options) (array usable in generateSW, vite‑plugin‑pwa and Serwist runtimeCaching), registerRasdRoutes(options) (for injectManifest/Serwist sw.ts), RasdOutboxPlugin (a fetchDidFail/sync bridge), and a prebuilt IIFE rasd-sw.js for importScripts / Expo workbox-cli.
  4. Fallback for apps without a SW: npx rasd-forms init public/ copies rasd-sw.js and the client registers it at a narrow scope (/rasd/) — enough for sync/push events without hijacking navigation caching (OneSignal pattern). Document the "keep the old file a year" migration warning.
  5. Sync engine is IndexedDB‑first: outbox + idempotency keys + foreground triggers; SyncManager and periodicSync are optional accelerators feature‑detected at runtime; never depend on BackgroundSyncPlugin semantics (network‑error only, replay needs an open page on Safari).
  6. Update‑safety contract: autosave every keystroke; export isDirty()/useRasdBusy(); document the Workbox waiting→prompt→messageSkipWaiting recipe and vite‑plugin‑pwa registerType:'prompt'; tell customers to hold updateServiceWorker() while a form is dirty.
  7. Budgets: form‑runner ≤ 120 KB gz JS (excluding React), builder lazy chunk separate, every chunk < 2 MB uncompressed (Workbox default), Arabic font subsetted; runtime caches rasd-defs-v{n} (StaleWhileRevalidate, maxEntries 200) and rasd-media-v{n} (CacheFirst, maxAgeSeconds 30 d, maxEntries 500, CacheableResponsePlugin 0/200).
  8. Install & persistence UX components: <InstallHint> (Chromium beforeinstallprompt button / iOS Add‑to‑Home‑Screen steps, iOS 26 note), requestPersistence() after first sync, diagnostics panel showing storage.estimate(), persisted(), standalone mode, queue length; warn Safari‑tab users about the 7‑day purge.
  9. Manifest guidance, not manifest ownership: docs template with name, short_name, id, start_url, display: standalone, 192/512 + maskable icons, screenshots, lang, dir: rtl where relevant; the customer's build plugin generates it.
  10. Push (optional module): standard Web Push + VAPID, payloads in Declarative Web Push JSON so iOS 18.4+ needs no SW; gate iOS behind installed detection.
  11. Framework recipes to publish and test in CI: Vite + vite‑plugin‑pwa (generateSW and injectManifest), Next 15/16 + Serwist (webpack and Turbopack), Expo web + workbox‑cli, plain HTML + IIFE. Drop next-pwa.
  12. CSP + licence offline: publish the CSP checklist above with SRI hashes; the licence token check must tolerate offline (grace period cached in IndexedDB) so field workers are never blocked — coordinate with the licensing research doc.
  13. Marketing: do not claim a "Lighthouse PWA score"; publish an offline‑readiness checklist and a rasd-forms doctor command instead.

Sources (accessed 2026‑08‑15)

  1. Chrome for Developers — workbox‑background‑sync module. https://developer.chrome.com/docs/workbox/modules/workbox-background-sync/
  2. GitHub — GoogleChrome/workbox Releases (v7.4.1, v7.4.0, v7.3.0, v7.0.0). https://github.com/GoogleChrome/workbox/releases
  3. npm registry — vite-plugin-pwa 1.3.0 metadata (published 2026‑05‑05; peers). https://registry.npmjs.org/vite-plugin-pwa
  4. npm registry — workbox‑build/workbox‑window/workbox‑core/workbox‑background‑sync 7.4.1; serwist & @serwist/next 9.5.12; next‑pwa 5.6.0; @ducanh2912/next‑pwa 10.2.9; @vite‑pwa/nuxt 1.1.1; @vite‑pwa/remix 0.2.0; survey‑react‑ui 3.0.0; @formio/js 5.5.1; @formio/react 6.2.1; @formbricks/js 5.0.0. https://registry.npmjs.org/
  5. Serwist — @serwist/next Getting started. https://serwist.pages.dev/docs/next/getting-started
  6. Vite PWA — injectManifest guide; Prompt for update; Auto update. https://vite-pwa-org.netlify.app/guide/inject-manifest.html , https://vite-pwa-org.netlify.app/guide/prompt-for-update , https://vite-pwa-org.netlify.app/guide/auto-update.html
  7. WebKit blog — Updates to Storage Policy (Safari 17, 2023‑08‑10). https://webkit.org/blog/14403/updates-to-storage-policy/
  8. WebKit blog — Full Third‑Party Cookie Blocking and More (7‑day script‑writable storage). https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/
  9. MDN — Storage quotas and eviction criteria. https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria
  10. Can I use — Background Sync API. https://caniuse.com/background-sync
  11. Chrome for Developers — Periodic Background Sync. https://developer.chrome.com/docs/capabilities/periodic-background-sync
  12. Chrome for Developers — Revisiting Chrome's installability criteria. https://developer.chrome.com/blog/update-install-criteria
  13. GitHub — Lighthouse issue #15535 "Remove PWA Category". https://github.com/GoogleChrome/lighthouse/issues/15535
  14. web.dev — What does it take to be installable? (updated 2024‑09‑19). https://web.dev/articles/install-criteria
  15. web.dev — How to provide your own in‑app install experience. https://web.dev/articles/customize-install
  16. WebKit blog — WebKit Features in Safari 26.0 (2025‑09‑15). https://webkit.org/blog/17333/webkit-features-in-safari-26-0/
  17. WebKit blog — Meet Declarative Web Push; web.dev — Persistent storage. https://webkit.org/blog/16535/meet-declarative-web-push/ , https://web.dev/articles/persistent-storage
  18. web.dev — Create an offline fallback page. https://web.dev/articles/offline-fallback-page
  19. Chrome for Developers — Handling service worker updates with immediacy. https://developer.chrome.com/docs/workbox/handling-service-worker-updates/
  20. Chrome for Developers — workbox‑window module. https://developer.chrome.com/docs/workbox/modules/workbox-window/
  21. Chrome for Developers — workbox‑build module (defaults). https://developer.chrome.com/docs/workbox/modules/workbox-build/
  22. Google — PageSpeed Insights release notes (Lighthouse 12, 2024‑05‑10). https://developers.google.com/speed/docs/insights/release_notes
  23. Serwist — @serwist/next overview. https://serwist.pages.dev/docs/next
  24. Serwist — Turbopack guide (@serwist/turbopack). https://serwist.pages.dev/docs/next/turbo
  25. SurveyJS — Get started (HTML/CSS/JavaScript). https://surveyjs.io/form-library/documentation/get-started-html-css-javascript
  26. SurveyJS — Render a Survey inside Shadow DOM (React). https://surveyjs.io/form-library/examples/render-survey-inside-shadow-dom/reactjs
  27. GitHub — Typeform/embed README. https://github.com/Typeform/embed/blob/main/packages/embed/README.md
  28. Form.io — Embedding a Form (developer guide). https://help.form.io/dev/form-embedding
  29. GitHub — formbricks/js README; Formbricks framework guides. https://github.com/formbricks/js , https://formbricks.com/docs/website-surveys/framework-guides
  30. Tally — Widgets introduction; Embed your form. https://developers.tally.so/widgets/introduction , https://tally.so/help/embed-your-form
  31. React — React v19 release post (custom elements support). https://react.dev/blog/2024/12/05/react-19
  32. Makerkit — Building Embeddable React Widgets. https://makerkit.dev/blog/tutorials/embeddable-widgets-react
  33. React — 'use client' reference; Vite — Building for Production / Library Mode. https://react.dev/reference/rsc/use-client , https://vite.dev/guide/build.html
  34. Expo — Progressive web apps (modified 2026‑06‑03). https://docs.expo.dev/guides/progressive-web-apps/
  35. OneSignal — OneSignal service worker. https://documentation.onesignal.com/docs/en/onesignal-service-worker
  36. Firebase — Receive messages in a JavaScript client. https://firebase.google.com/docs/cloud-messaging/js/receive
  37. MSW — Browser integration. https://mswjs.io/docs/integrations/browser
  38. Privacy Sandbox — Storage partitioning (Chrome 115+). https://privacysandbox.google.com/cookies/storage-partitioning
  39. GitHub — create‑react‑app PR #10048 (raise maximumFileSizeToCacheInBytes); excalidraw issue #9354. https://github.com/facebook/create-react-app/pull/10048 , https://github.com/excalidraw/excalidraw/issues/9354
  40. GitHub — shadowwalker/next-pwa (archived 2023‑08‑18). https://github.com/shadowwalker/next-pwa
  41. SurveyJS — v3.0 major update. https://surveyjs.io/stay-updated/major-updates/2025-2026
  42. MDN — CSP worker-src directive. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src