PWA Plugin
@farm.js/pwa turns a production Farm build into an installable, offline-aware Progressive Web App.
It generates a service worker from the final client output, maps emitted static routes to their HTML
files, and registers the worker through Farm's browser lifecycle. Advanced applications can replace
the generated worker with a prebuilt JavaScript file while retaining that registration lifecycle.
Install
pnpm add @farm.js/pwaStart with the short configuration
import { defineConfig } from "@farm.js/core";
import { pwa } from "@farm.js/pwa";
export default defineConfig({
plugins: [
pwa({
offline: "/offline",
cache: "auto",
}),
],
});The generated worker always precaches immutable build assets. cache: "auto" additionally means:
- Precache every HTML page Farm emitted as a static route.
- Cache public same-origin images with SWR.
- Keep up to 100 image responses fresh for 30 days.
auto is the default, so you can omit cache for the same behavior. The previous recommended
value remains available as a compatibility alias. Service worker updates separately default to
update: "prompt".
The generated worker only intercepts GET requests and same-origin URLs. Dynamic pages, APIs,
actions, integrations, and workflows remain network-owned.
Add the offline page
The fallback has to be static because the worker must download it during installation.
export const ssg = true;
export default function OfflinePage() {
return (
<main>
<h1>You are offline</h1>
<p>Reconnect to load fresh server data.</p>
</main>
);
}The production build fails with a clear message if offline does not resolve to emitted static
HTML. This prevents a worker from installing with a fallback that can never work.
Add the application manifest
Use Farm's existing metadata route. Farm serves it at /manifest.webmanifest, so the plugin does
not ask you to repeat application metadata in PWA configuration.
import type { MetadataRoute } from "@farm.js/core";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Acme Field App",
short_name: "Acme",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#16a34a",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}SWR in one line
Use the explicit form when you want individual cache controls instead of the automatic preset:
pwa({
cache: {
staticRoutes: true,
images: "swr",
},
});swr means stale while revalidate:
- The first request downloads and stores the image.
- A later request returns the cached image immediately.
- The worker fetches a newer response in the background.
- The next request receives the refreshed image.
Cache Storage is shared by every account that signs in on the same browser profile and survives sign-out, so the worker only stores an image it can safely show to anyone:
- The response is marked shareable with
Cache-Control: publicor a positivemax-age, or has a futureExpires. - Or it looks like a static file: no
Cache-Controlfreshness, but aLast-Modifiedheader. Static files inpublic/are served this way.
An image is never stored when its request has an Authorization header, when its response is
marked private, no-store, no-cache, or max-age=0, or when it varies on Cookie,
Authorization, or *. A response with no caching headers at all is not stored either, which is
what an API route returning a per-account image usually sends. To have a dynamic image cached, send
an explicit policy such as Cache-Control: public, max-age=3600, and only for images that are the
same for every visitor.
Runtime image caching is best-effort. If browser storage is unavailable or full, a successful network response still loads normally. If the network fails, an available cached image remains the fallback; without one, the network error is preserved. Install-time precaching still requires all selected assets to be cached successfully.
Customize the storage bounds with short names:
pwa({
cache: {
staticRoutes: ["/", "/help", "/pricing"],
images: {
strategy: "swr",
limit: 200,
ttl: "7d",
},
},
});limit is the maximum image count. ttl accepts milliseconds or s, m, h, d, and w
durations such as 30s, 5m, 6h, 30d, or 2w.
Bring your own service worker
Use a custom worker when you need complete control over fetch routing, background sync, push notifications, or a caching strategy outside the generated worker's scope:
pwa({
serviceWorker: {
source: "src/service-worker.js",
type: "module",
},
update: "prompt",
});source is relative to the Farm project root. Farm copies it verbatim to the final sw.js path
under basePath; it does not bundle or transform the file. Module imports must therefore resolve
to files available in the production public output.
The custom worker owns install, fetch, offline, and cache behavior, so serviceWorker cannot be
combined with offline or cache. Farm still registers the worker and exposes its update events.
To support applyUpdate, handle the message used by Farm's update lifecycle:
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("message", (event) => {
if (event.data?.type === "FARM_PWA_SKIP_WAITING") void self.skipWaiting();
});If you need to control registration and scope as well, leave out the PWA plugin and register the worker directly in application client code.
Update behavior
The default is update: "prompt". When a new service worker finishes installing, the plugin
dispatches an event instead of reloading a page that may contain unsaved work:
window.addEventListener("farm:pwa:update-available", (event) => {
const { applyUpdate } = (event as CustomEvent).detail;
showUpdateButton({ onClick: applyUpdate });
});Application code can also call:
window.__FARM_PWA__?.applyUpdate();Set update: "auto" only when reloading cannot discard user input. Auto mode activates a waiting
worker and reloads once the new worker controls the page.
Options
Adding pwa() to plugins enables service worker generation and registration. Remove it from the
array when the app should not ship a service worker.
| Option | Default | Description |
|---|---|---|
offline | false | Static fallback route for the generated worker. |
update | "prompt" | Prompt or automatically activate and reload for a waiting worker. |
cache | "auto" | Generated-worker caching, a custom object, or build assets only. |
serviceWorker | false | Prebuilt worker source and optional "classic" or "module" type. |
| Cache option | Default under auto | Description |
|---|---|---|
staticRoutes | true | Every emitted static page, a route list, or false. |
images | "swr" | SWR options, true, "swr", or false. |
offline and explicit staticRoutes entries must be stable root-relative application paths.
URLs, query strings, fragments, dot segments, backslashes, control characters, and encoded path
separators are rejected.
Write these paths without the deployment prefix: with basePath: "/app", "/" is served at
/app and "/help" at /app/help. An application route named "/app" is distinct from the home
page and is cached at /app/app. Farm handles localized HTML output automatically; include the
locale in a localized route such as offline: "/en/offline", but do not add the deployment prefix.
Production lifecycle
During farm build, generated-worker mode:
- Finds the preset's final public output.
- Hashes precached files and caching options into a deployment-specific cache ID.
- Writes
sw.jsunder Farm's configuredbasePath. - Maps clean static route URLs to emitted HTML files.
- Fails if the configured offline page is missing.
Generated cache names are isolated by basePath, so multiple Farm applications on one origin do
not delete or reuse a sibling application's precache or image cache.
Custom-worker mode copies the configured source to the same deployment-aware sw.js location and
leaves its contents untouched.
In the browser, the plugin registers only for production builds. Service workers require HTTPS in production; browsers also permit localhost for development and local production testing.
See the complete runnable source in
examples/pwa-demo.
Generate a route-aware service worker with safe offline navigation, update prompts, and concise SWR image caching.