AbsoluteJS

PWA

@absolutejs/pwav0.16.2betaFrontend & UX

Framework-agnostic PWA and Web Push primitives — manifest, push service worker, VAPID sender, and browser subscription glue — for any Bun or Elysia app.

@absolutejs/pwa provides framework-agnostic primitives for turning any app into an installable, push-capable PWA: a web app manifest builder, a generated push service worker, a VAPID Web Push sender that flags dead endpoints, and browser glue for registration and subscription. It is storage- and framework-agnostic — you decide how subscriptions are stored and how routes are mounted, which makes it a natural fit for an Elysia server on Bun. Server helpers live at the package root; browser helpers at @absolutejs/pwa/client.

#Installation

BASH
bun add @absolutejs/pwa

#Capabilities

Overview

Framework-agnostic primitives for turning any app into an installable, offline-capable, push-capable PWA: a web app manifest, finite background Sync, the push service worker, a VAPID Web Push sender that flags dead endpoints, and browser glue for service-worker registration + subscription.

Its direct primitives remain storage- and framework-agnostic. AbsoluteJS builds add the recommended path: Auth owns registration, Dispatch owns lifecycle and fanout, and page code uses only @absolutejs/devices.

Server

Mounting is yours. With Elysia:

Portable push in AbsoluteJS

In an AbsoluteJS application, importing pushNotifications and enabling pwa automatically generates the browser adapter and subscription-rotation worker. Set the public VAPID_PUBLIC_KEY in the build environment; keep VAPID_PRIVATE_KEY only in the trusted delivery process. Configure Auth once with a Dispatch lifecycle at auth({ push: ... }). The browser can submit only its provider credential and opaque installation ID—Auth derives user, tenant, and topics on the server.

The same page code uses APNs/FCM in a Capacitor shell and Web Push in a browser. For direct PWA integrations, pass push to registerServiceWorker() or call configurePwaPush() explicitly. Installation identity is shared with the service worker in IndexedDB so browser subscription rotation updates the same trusted server record even when no page is open.

Dispatch users can import createWebPushDispatchAdapter from @absolutejs/pwa/dispatch; it maps provider-neutral messages to the Web Push sender and classifies gone/retryable results for lifecycle retirement.

Client

Offline Sync

@absolutejs/pwa can provision @absolutejs/sync automatically for an authenticated browser app. Generate the worker with sync: true, then enable the client bridge during registration:

The Sync browser runtime is loaded lazily only when this bridge is enabled, so manifest-, install-, and push-only applications do not pay its client cost.

Show 10 more

AbsoluteJS supplies storageSchema automatically by composing the app schema with declarative metadata from installed Sync packs. The same JSON-safe plan is used by the foreground client and service worker, so IndexedDB upgrades before either runtime reads cached rows or flushes the durable outbox. Direct PWA integrations may pass that component bundle through sync.storageSchema.

The bundle also carries local-data sensitivity, retention, and quota rules. Browsers do not receive pretend encryption backed by a key stored beside the ciphertext: an encryption-required record fails closed unless the direct PWA integration supplies an audited Sync protection provider. Portable packs can instead declare onProtectionUnavailable: "memory-only"; the feature keeps its live browser state and online-only mutations without writing sensitive rows or arguments to IndexedDB, while the same bundle is durably encrypted by the native Keychain/Keystore-backed adapter. Quota pressure evicts complete cached projections and never the durable outbox.

With @absolutejs/auth and syncSocket() mounted, no page-level token or collection list is needed. The client performs a strict same-origin JSON POST to /__absolute/sync/principal using the existing HTTP-only session cookie. The response contains only an opaque account namespace. Foreground Sync clients and the worker share a namespaced IndexedDB store; the worker discovers persisted id-keyed collection descriptors, flushes the durable outbox, and pulls finite updates through /__absolute/sync/background.

Background Sync is best-effort because browser support and scheduling vary. Online, focus, and visible-page events also reconnect and flush active clients, so foreground/resume remains the correctness path. Native apps continue using their Bearer-based Auth transport rather than this cookie-session bridge.

Advanced endpoints, limits, and the shared database name are configurable:

Both endpoints must resolve to the page's exact origin and redirects are refused. Cookies, bearer tokens, mutation arguments, and application rows are never embedded in the worker script or service-worker messages. If the session is absent, the worker configuration is cleared. On focus, online, and visible page transitions, the bridge resolves the principal again before syncing. An account change aborts the old run, clears its runtime, and installs the new opaque namespace before any new work starts. configurePwaSync() is also exported for hosts that register their worker separately.

Observe sanitized run outcomes without sending application data to AbsoluteJS:

The same aggregate result is dispatched on window as the absolute:pwa-sync-result custom event. Its contract permits only status, duration, trigger, and aggregate counts; namespaces, endpoints, credentials, mutation arguments, and rows are discarded at the page boundary. This is an application-observable signal, not built-in analytics or a phone-home channel.

Install prompt

Capture the browser's install signal and drive it from your own button:

Web app manifest

createWebAppManifest builds a typed web app manifest — icons, shortcuts, share targets — ready to serve as application/manifest+json.

Push service worker

pushServiceWorker generates the service worker source, with optional offline support that precaches an app shell and serves a fallback page.

VAPID sender

createWebPush signs and sends Web Push notifications, fans out to all of a user’s devices with sendMany, and reports gone endpoints for pruning.

Browser subscription glue

registerServiceWorker, subscribeToPush, unsubscribeFromPush, and getPushStatus handle the browser side; you POST the results to your own routes.

Install prompt control

initInstallPrompt captures the install signal so you can show your own install button and trigger promptInstall from a user gesture.

Feature-safe everywhere

Every client function no-ops when the APIs are missing or during SSR, and the sender no-ops when VAPID keys are unset — push degrades gracefully.

Capability-aware embedded browser UX

getBrowserCapabilities reports install, push, service-worker, passkey, media, clipboard, and sharing support. detectEmbeddedBrowser conservatively identifies Facebook, Instagram, and Messenger only when explicit host markers are present.

#Capability-first browser UX

Feature checks are authoritative. Embedded-browser identity should only explain why an action is unavailable.

  1. Detect
    Read the current browser capability snapshot.
  2. Offer
    Offer install, push, sharing, passkeys, or media only when supported.
  3. Explain
    Use conservative host-app detection to explain unavailable actions.
  4. Recover
    Provide an open-in-browser or alternate-channel fallback.

Outcomes

What you can build

Overview

Framework-agnostic primitives for turning any app into an installable, offline-capable, push-capable PWA: a web app manifest, finite background Sync, the push service worker, a VAPID Web Push sender that flags dead endpoints, and browser glue for service-worker registration + subscription.

Server

Mounting is yours. With Elysia:

Portable push in AbsoluteJS

In an AbsoluteJS application, importing pushNotifications and enabling pwa automatically generates the browser adapter and subscription-rotation worker. Set the public VAPID_PUBLIC_KEY in the build environment; keep VAPID_PRIVATE_KEY only in the trusted delivery process. Configure Auth once with a Dispatch lifecycle at auth({ push: ... }). The browser can submit only its provider credential and opaque installation ID—Auth derives user, tenant, and topics on the server.

Hardening checklist

Production guidance

Make every external boundary explicitPin the deployed @absolutejs/pwa version, replace example or memory-backed dependencies with durable implementations, bound external calls, protect credentials, and emit enough evidence to retry or recover safely.

Follow in order

Troubleshooting path

1
Trace from the first failed boundary
Reproduce the smallest canonical @absolutejs/pwa example, confirm the supported entry point and version in the API explorer, then inspect the first boundary that did not produce its documented result.

#@absolutejs/pwa quick start

Partial snippet

# @absolutejs/pwa

BASH
bun add @absolutejs/pwa

#Install prompt

Partial snippet

Capture the browser's install signal and drive it from your own button:

TS
import {
  initInstallPrompt,
  onInstallable,
  promptInstall,
  canInstall,
} from "@absolutejs/pwa/client";

initInstallPrompt(); // once at boot

// React to availability (show/hide your install button):
const off = onInstallable((installable) => setShowInstall(installable));

// From a click handler (must be a user gesture):
const accepted = await promptInstall();

#Server

Partial snippet

Working example for Server.

TS
import {
  createWebAppManifest,
  pushServiceWorker,
  createWebPush,
} from "@absolutejs/pwa";

const ICON = "/icons/app-512.png";

// Serve as application/manifest+json at /manifest.webmanifest
export const manifest = createWebAppManifest({
  name: "My App",
  shortName: "MyApp",
  themeColor: "#6366f1",
  icons: [
    { src: ICON, sizes: "192x192", type: "image/png", purpose: "any" },
    { src: ICON, sizes: "512x512", type: "image/png", purpose: "any" },
  ],
});

// Serve as text/javascript at /sw.js with header `Service-Worker-Allowed: /`.
// Pass `offline` to also precache an app shell + serve a fallback page when a
// navigation fails offline, and cache same-origin assets cache-first.
export const sw = pushServiceWorker({
  icon: ICON,
  offline: { fallback: "/offline.html", assetPrefix: "/assets/" },
  sync: true,
});

// VAPID sender — pass empty/unset keys and it no-ops (isConfigured() === false),
// so push degrades gracefully to your email/in-app fallback.
const push = createWebPush({
  publicKey: process.env.VAPID_PUBLIC_KEY,
  privateKey: process.env.VAPID_PRIVATE_KEY,
  subject: "mailto:you@example.com",
});

// Fan out to a user's devices; prune whatever it reports gone.
const { gone } = await push.sendMany(subscriptions, {
  title: "New match",
  body: "Acme Co. just replied.",
  url: "/inbox",
});
await pruneEndpoints(gone); // your storage

#Server 2

Partial snippet

Mounting is yours. With Elysia:

TS
new Elysia()
  .get("/manifest.webmanifest", ({ set }) => {
    set.headers["content-type"] = "application/manifest+json";
    return manifest;
  })
  .get("/sw.js", ({ set }) => {
    set.headers["content-type"] = "text/javascript";
    set.headers["service-worker-allowed"] = "/";
    return sw;
  });

#Server

Partial snippet

Serve the manifest and service worker from Elysia, then send Web Push notifications with the VAPID sender.

TS
import {
	createWebAppManifest,
	createWebPush,
	pushServiceWorker
} from '@absolutejs/pwa';
import { Elysia } from 'elysia';

const ICON = '/icons/app-512.png';

const manifest = createWebAppManifest({
	icons: [
		{ purpose: 'any', sizes: '192x192', src: ICON, type: 'image/png' },
		{ purpose: 'any', sizes: '512x512', src: ICON, type: 'image/png' }
	],
	name: 'My App',
	shortName: 'MyApp',
	themeColor: '#6366f1'
});

// Pass `offline` to also precache an app shell and serve a fallback
// page when a navigation fails offline.
const sw = pushServiceWorker({
	icon: ICON,
	offline: { assetPrefix: '/assets/', fallback: '/offline.html' }
});

new Elysia()
	.get('/manifest.webmanifest', ({ set }) => {
		set.headers['content-type'] = 'application/manifest+json';

		return manifest;
	})
	.get('/sw.js', ({ set }) => {
		set.headers['content-type'] = 'text/javascript';
		set.headers['service-worker-allowed'] = '/';

		return sw;
	});

// VAPID sender — with empty/unset keys it no-ops, so push degrades
// gracefully to your email or in-app fallback.
const push = createWebPush({
	privateKey: process.env.VAPID_PRIVATE_KEY,
	publicKey: process.env.VAPID_PUBLIC_KEY,
	subject: 'mailto:you@example.com'
});

// Fan out to a user's devices; prune whatever it reports gone.
const { gone } = await push.sendMany(subscriptions, {
	body: 'Acme Co. just replied.',
	title: 'New match',
	url: '/inbox'
});
await pruneEndpoints(gone); // your storage

#Client

Partial snippet

Register the service worker and manage push subscriptions from the browser; you own the routes the results are posted to.

TS
import {
	getPushStatus,
	registerServiceWorker,
	subscribeToPush,
	unsubscribeFromPush
} from '@absolutejs/pwa/client';

// At boot:
await registerServiceWorker(); // defaults to "/sw.js"

// Toggle on: returns the subscription JSON — POST it to your own route.
const subscription = await subscribeToPush(vapidPublicKey);
await fetch('/push/subscribe', {
	body: JSON.stringify(subscription),
	method: 'POST'
});

// Toggle off: returns the endpoint to drop server-side.
const endpoint = await unsubscribeFromPush();
await fetch('/push/unsubscribe', {
	body: JSON.stringify({ endpoint }),
	method: 'POST'
});

const status = await getPushStatus();
// { supported, permission, subscribed }

#Install Prompt

Partial snippet

Capture the browser install signal and drive installation from your own button instead of the default mini-infobar.

TS
import {
	canInstall,
	initInstallPrompt,
	onInstallable,
	promptInstall
} from '@absolutejs/pwa/client';

initInstallPrompt(); // once at boot

// React to availability (show/hide your install button):
const off = onInstallable((installable) => setShowInstall(installable));

// From a click handler (must be a user gesture):
const accepted = await promptInstall();

#Embedded Browser Capabilities

Partial snippet

Feature-detect before offering browser-dependent actions, then use host-app identity only to explain an unavailable capability.

TS
import {
	detectEmbeddedBrowser,
	getBrowserCapabilities
} from '@absolutejs/pwa/client';

const capabilities = getBrowserCapabilities();
if (!capabilities.pushNotifications && capabilities.embeddedBrowser) {
	showOpenInBrowserHelp(capabilities.embeddedBrowser.app);
}

// Pure classification is available for SSR and tests.
detectEmbeddedBrowser(request.headers.get('user-agent') ?? '');
Bring your own routes and storage
Mounting is yours: serve the manifest as application/manifest+json and the service worker as text/javascript with a Service-Worker-Allowed header, from any framework. Subscriptions land in whatever storage you already use.

Current package surface

What ships today

@absolutejs/pwav0.16.2 · betaFrontend & UXnpmSource
5entry points

Import surface · click to copy