API Reference

withFlux()

A Next.js config wrapper that generates your service worker at build time and injects the hydration race fix script into every page. The one call that connects Flux's IDB layer to offline navigation and asset caching.

Prerequisites
  • @tsworldtech/flux-next installed
  • A storagePrefix that matches the one passed to createFlux(), see api/create_flux
  • idbKey and listKey values that match the channels you register with flux.register()
1

Basic usage

Import withFlux from @tsworldtech/flux-next/plugin. It takes the Flux config first and returns a function that wraps your Next.js config, so it composes with any other config wrappers you already use.
typescript
1// next.config.ts
2import type { NextConfig } from "next";
3import { withFlux } from '@tsworldtech/flux-next/plugin'
4import { PRECACHE_ROUTES } from './config/precache-routes'
5
6const nextConfig: NextConfig = {
7 // your existing config
8}
9
10// withFlux takes the Flux config first, then returns a function that
11// wraps your Next.js config. It composes with any other wrappers you use.
12export default withFlux({
13 precacheRoutes: [...PRECACHE_ROUTES],
14 offlineShell: '/offline',
15 storagePrefix: 'Flux-App',
16 syncTags: ['flux:sync:default'],
17 dynamicRoutePrefixes: ['/portal/projects/'],
18 bypassRoutePrefixes: ['/portal/auth/callback'],
19})(nextConfig)
2

Config reference

precacheRoutes, offlineShell, and storagePrefix are the fields you will set on nearly every project. storagePrefix specifically must match whatever you pass to createFlux(), since the SW and the engine need to agree on the same IDB namespace. The dynamic route fields are only needed if your app has routes like /projects/[slug] that must work offline.
typescript
1interface GenerateSwConfig {
2 precacheRoutes: string[] // routes cached ahead of time, not just on first visit
3 offlineShell: string // fallback route served when nothing else matches
4 storagePrefix: string // must match the storagePrefix passed to createFlux()
5 syncTags?: string[] // Background Sync tags registered while a tab is hidden
6 cacheVersion?: string // bump to force old SW caches to be dropped
7 outDir?: string // where sw.js is written, defaults to /public
8 dynamicRoutePrefixes: string[] // see step 5, enables offline dynamic route rendering
9 dynamicRouteShapes?: { // see step 6, nested route shapes under each prefix
10 prefix: string
11 subpaths: string[]
12 }[]
13 dynamicRouteIdbKeys?: Record< // see step 7, how the SW checks IDB for route data
14 string,
15 {
16 idbKey: string
17 listKey?: string
18 listFields?: string[]
19 }
20 >
21 bypassRoutePrefixes?: string[] // see step 8, routes the SW never touches
22}
3

What happens at build time

Two things happen automatically. You never hand write or edit the generated service worker file yourself.
typescript
1// withFlux() wraps your Next.js config and, at build time:
2// 1. Writes sw.js to /public (or outDir) using the SW template, with
3// your config values injected as PRECACHE_ROUTES, CACHE_VERSION,
4// IDB_NAME, OFFLINE_SHELL, SYNC_TAGS, EXTERNAL_CACHE_ORIGINS,
5// DYNAMIC_ROUTE_PREFIXES, and your dynamic route shape, IDB key
6// and bypass settings
7// 2. Injects an inline script into <head> that sets
8// window.FLUX_SW_READY = false, resolving it once
9// navigator.serviceWorker.ready fires
10
11// You never write or touch sw.js yourself, it is fully generated.
4

The SW vs App Router hydration race fix

This is the specific problem withFlux() exists to solve: without a signal for "the service worker is actually controlling this page yet," the App Router can render before the SW is ready to serve cached assets, causing a flash of unstyled or unhydrated content on repeat visits.
typescript
1// The inline script withFlux() injects, conceptually:
2window.FLUX_SW_READY = new Promise((resolve) => {
3 if (!('serviceWorker' in navigator)) return resolve(true)
4 navigator.serviceWorker.ready.then(() => resolve(true))
5})
6
7// HydrationGate waits on BOTH flux.hydrated and window.FLUX_SW_READY.
8// On a repeat visit the SW is already active, so this resolves in
9// well under 10ms. On a first visit, the 2000ms hard timeout in
10// HydrationGate takes over and the app renders unhydrated.
Pairs directly with HydrationGate

window.FLUX_SW_READY only matters because HydrationGate reads it. If you are not using HydrationGate, this script still runs but nothing consumes it by default.

5

dynamicRoutePrefixes

This is what makes a dynamic route like /portal/projects/[slug] work fully offline, even for a specific slug the user has never visited before, as long as at least one page has been visited once. Every prefix needs a trailing slash.
typescript
1dynamicRoutePrefixes: ['/portal/projects/', '/portal/keys/manage-subscription/']
2
3// A hard navigation to /portal/projects/proj_abc while fully offline,
4// having never visited that exact URL before:
5// 1. Misses PAGE_CACHE (never fetched this specific page)
6// 2. Misses the network (offline)
7// 3. pathname matches a prefix in DYNAMIC_ROUTE_PREFIXES
8// 4. SW serves a cached app shell AT THE REAL URL, so the address
9// bar stays on /portal/projects/proj_abc
10// 5. Next.js's client router mounts the matching route component
11// 6. Flux hydrates the actual item data from IDB
12
13// Without a matching prefix, step 3 falls through to offlineShell instead.
6

dynamicRouteShapes

A prefix on its own only covers the bare detail page. If the dynamic segment has nested routes underneath it, declare each one as a subpath so the SW knows every shape that exists.
typescript
1dynamicRouteShapes: [
2 {
3 prefix: "/portal/projects/",
4 subpaths: ["", "/license", "/domains", "/alerts", "/settings", "/usage"],
5 },
6 {
7 // manage-subscription/[slug] is the only page under this prefix
8 // with no nested routes, so the bare detail shell is all that is needed.
9 prefix: "/portal/keys/manage-subscription/",
10 subpaths: [""],
11 },
12]
13
14// Each entry describes the page shapes that exist under a dynamic prefix.
15// "" is the bare detail page, and every other subpath is a nested route
16// such as /portal/projects/[slug]/license.
17//
18// This lets the SW hold a shell for every shape, so an offline navigation
19// to a nested route lands on the right page instead of the bare detail one.
20// It also replaces hand written shell entries in your precacheRoutes list.
7

dynamicRouteIdbKeys

This tells the SW how to decide whether it has data for a dynamic route while offline. It looks up the item in IDB by the URL slug, first through the detail channel and then through the list channel as a fallback. The keys of this object must match dynamicRoutePrefixes exactly.
typescript
1dynamicRouteIdbKeys: {
2 '/portal/projects/': {
3 idbKey: 'project_detail',
4 listKey: 'projects:list',
5 listFields: ['projectId', 'id'],
6 },
7 '/portal/keys/manage-subscription/': {
8 idbKey: 'key_detail',
9 listKey: 'keys:list',
10 listFields: ['id'],
11 },
12}
13
14// Route prefix -> where the SW looks to decide "do we have data for this
15// route offline?". Keys must match dynamicRoutePrefixes exactly, trailing
16// slash included.
17//
18// idbKey: the per item detail channel. Tried first with an exact
19// key get: '{scope}:{idbKey}:{slug}'. Only hits when the
20// URL slug equals the detail entry's key (the UUID id).
21//
22// listKey: the idbKey of the whole list channel, as registered with
23// flux.register (for example 'projects:list'). Used as a
24// fallback when the exact key misses, such as when the URL
25// uses projectId ('proj_f963e98a-5f5') but the detail entry
26// is keyed by the UUID. One get on the list, then an
27// in memory match.
28//
29// listFields: item fields compared against the URL slug.
30//
31// These strings must match the idbKey values in your registered channels exactly.
Why listKey exists

If your URLs use a human readable identifier while the detail entry is keyed by a UUID, the exact key lookup will miss. listKey and listFields let the SW find the item by scanning the cached list instead.

8

bypassRoutePrefixes

Some routes must never be handled by the service worker. List their prefixes here and navigations to them go straight to the network.
typescript
1bypassRoutePrefixes: ['/portal/auth/callback']
2
3// Any navigation whose pathname starts with a bypass prefix goes straight
4// to the network. The SW does not cache it, does not serve it from cache,
5// and does not fall back to the offline shell for it.
6//
7// Use this for routes that must always run live, such as OAuth callbacks
8// and other one time redirect handlers.
9

What the generated service worker does

You do not write any of this. It is worth knowing what is running, since it is the layer between a navigation and whatever withFlux() configured for you.
typescript
1// What the generated sw.js actually does at runtime:
2
3// handleNavigation: cache first with background update. On a miss for
4// both cache and network, checks DYNAMIC_ROUTE_PREFIXES (and the route
5// shapes and IDB keys you configured) before falling back to the
6// offline shell. Paths under a bypass prefix are never intercepted.
7
8// handleStatic: cache first for every /_next/static/ path and static
9// file extension.
10
11// FLUX_WARM_CHUNKS listener: receives the assets[] array from a
12// bootstrap() response and fetches and caches any chunks not already stored.
13
14// SKIP_WAITING listener: forces the new SW to activate immediately
15// when you post that message, instead of waiting for all tabs to close.

For the hook that consumes window.FLUX_SW_READY, see api/hydrate. For how assets[] reaches the FLUX_WARM_CHUNKS listener in the first place, see api/bootstrap. Building with Vite or Remix instead of Next.js? See the Vite plugin covered under the flux-react package docs, which uses the same SW template with no App Router specifics.

Powered by Flux