library
PWA Kit¶
active
Shared Web Push transport and embedded PWA notification helpers for Go applications.
Shared Web Push transport and PWA notification helpers for Go applications. Browser and service-worker scripts are embedded in the Go module, so one pinned module version upgrades all three layers. No CDN, npm install or browser build step is required by consuming apps.
Install¶
go get github.com/kilo666mj/[email protected]
pwa-kit requires Go 1.26.5 or newer and tests the minimum and current Go
releases. Browser and worker helpers are versioned with the Go module, so pin
one module tag rather than copying the scripts. See the Go API on
pkg.go.dev.
Mount the scripts before the app's fallback route:
Load /pwa-kit/browser.js before your app script. Import
/pwa-kit/worker.js from your app's service worker. AssetVersion is a content
fingerprint: include it in your app's asset version so upgrades invalidate its
cached shell. The scripts use Cache-Control: no-cache; service-worker
registration uses updateViaCache: 'none'.
Application boundaries¶
| Shared kit | Application |
|---|---|
| VAPID contact normalization and key-pair validation | Secret storage and key provisioning |
Bounded subscription JSON decoder, including expirationTime: null |
Authentication, same-origin checks and subscription ownership |
| One push delivery, a public-address-only HTTP client and sanitized provider errors | Recipients, payloads, concurrency, TTL, urgency and retry policy |
| Permission and subscription lifecycle | Controls, styling, product wording and install UI |
| Visible push notifications, badges and safe click navigation | Worker activation, caching and optional message prefetch |
Each app keeps its own keys and subscriptions. This is a library, not a shared
notification service. Subscription.Validate checks shape and limits.
NewPublicHTTPClient resolves at dial time, refuses private and special-use
addresses, dials the validated IP directly, disables proxies, and rejects
redirects. Send uses that protected client when HTTPClient is nil; callers
that override it own equivalent outbound protections.
Server¶
Construct a Config{PublicKey, PrivateKey, Contact} and call Validate at startup.
Both [email protected] and mailto:[email protected] are
accepted, as is an HTTPS contact URL. Known local-only hosts such as .internal
and localhost are rejected before delivery. Validation does not prove that a
mailbox or public hostname exists; provide a real contact.
Use DecodeSubscription(r.Body) on your authenticated enrollment route. It
accepts one JSON object, rejects unknown fields, and limits the body to 16 KiB.
Store the resulting subscription under the authenticated user's identity.
outboundClient := pwakit.NewPublicHTTPClient(15 * time.Second)
result, err := pwakit.Send(ctx, config, subscription, payload, pwakit.Options{
TTL: 300,
Urgency: "normal",
HTTPClient: outboundClient,
})
if result.Expired() {
// Remove only this user's expired subscription from your store.
}
if err != nil {
logger.Warn("push delivery failed", "error", err)
}
TTL is explicit: zero stays zero, allowing apps to prevent queued notifications
from replaying later. The kit performs no implicit retry. Result.Retryable()
classifies 429 and 5xx responses; the app chooses when and whether to retry.
DeliveryError omits endpoints, keys, payloads and raw response bodies while
retaining known provider reasons such as BadJwtToken and VapidPkHashMismatch.
An accepted response proves provider acceptance, not on-device display.
Browser¶
See the minimal enrollment example. Configure
PWAKit.createPushClient with:
getPublicKey(): return the app's VAPID public key.save(subscription)and optionalremove(subscription): persist/delete via authenticated app APIs; reject on non-success responses.onState(state): renderchecking,enabling,disabling,on,off,blocked,install,unsupported,signedOutorerror.- Optional
isAuthenticated(),workerURL,registration(),renewMissing,storageKey,timeoutMsandwatch.
Call refresh() after sign-in. Call enable() directly from the click handler,
without awaiting other work first; Safari requires the original user gesture.
disable() removes server registration before unsubscribing. Explicit disables
are remembered on this origin, so renewMissing: true does not silently undo
them. on is emitted only after server registration succeeds. Reopening or
focusing the app rechecks permission and restores existing server registration.
Use destroy() when replacing a page or signing out; it removes event listeners.
It suppresses later UI callbacks, but does not cancel in-flight app API requests.
client.registration() supports local notification previews; a local preview
must not be described as a test of server delivery.
Worker¶
importScripts('/pwa-kit/worker.js');
PWAKitWorker.installPushHandlers({
title: 'My app', icon: '/icons/icon-192.png', tag: 'my-app-update',
notificationOptions: data => ({requireInteraction: Boolean(data.urgent)}),
afterPush: data => prefetchAppContent(data.url)
});
A notification is displayed immediately, even if optional badging or prefetch
fails. Numeric data.badge updates the app badge; override badgeCount(data) to
return a nonnegative number or 'dot'. Click targets are limited to the app's
origin, and failed navigation of an existing window falls back to opening one.
Caching and worker install/activate handlers remain in your app.
New-app adoption checklist¶
- Pin this module and serve its embedded scripts from the app origin.
- Supply durable per-app keys and a real public VAPID contact; validate at startup.
- Adapt authenticated, user-owned subscription storage and use the protected
default transport or
NewPublicHTTPClient. - Connect visible, keyboard-accessible controls to the shared state callbacks.
- Keep notification policy and worker caching explicit in the app.
- Test permission denial, failed saves, reload/resume and notification clicks.
- On a physical iPhone, install from Safari, grant permission from a tap, close the app, send through the server, and confirm receipt. Browser mocks cannot establish actual APNs/device delivery.
Use examples/minimal as the starting point for
new integrations. Its server is
loopback-only and intentionally does not persist subscriptions or send pushes.
Set PWA_PUBLIC_KEY, PWA_PRIVATE_KEY and PWA_CONTACT, then run
go run ./examples/minimal.
Development¶
gofmt -w .
go test ./...
go vet ./...
node --check assets/browser.js
node --check assets/worker.js
node --test tests/*.test.cjs
The transport tests use generated keys and a captured HTTP request. Browser and worker tests cover user-gesture timing, save failures, revoked permission, explicit disable, renewal, startup timeout recovery and notification navigation.