Developer documentation

oPWA v1.0.0

Transforms any WordPress site into a fully capable Progressive Web App — manifest, service worker, self-hosted push, install prompt, and analytics, all without a third-party push service.

WordPress plugin WP 6.0 · PHP 7.4+ HTTPS required v1.0.0 · Orravo
01 · Overview

What oPWA does

A complete PWA stack for WordPress — no FCM, no third-party service. Generates the manifest, dynamic service worker, and handles encrypted push end-to-end.

📄
Web App Manifest
App name, icons, theme colours, screenshots, share target
⚙️
Service Worker
5 caching strategies, custom URL routing, precache, offline fallback
🔔
Push Notifications
Self-hosted VAPID (RFC 8030), AES-128-GCM encrypted payloads
📲
Install Prompt
5 trigger modes, customisable banner, iOS share instructions
🔄
Background Sync
IndexedDB form queue, syncs on reconnect
💾
Cache Management
Live stats via SW postMessage, clear all / by URL
📊
Analytics
Beacon-based: page views, SW coverage, cache hit rate, installs
🛒
WooCommerce Mode
Network-only for checkout / cart / account URLs
⌨️
WP-CLI
6 commands for headless management
🔧
Developer API
7 filters and 4 actions for customisation
02 · Requirements

What you need

WordPress
6.0+
PHP
7.4+
8.1+ recommended
Extensions
openssl+
gmp for PHP < 8.1
Protocol
HTTPS*
Required for SW
ComponentMinimumNotes
WordPress6.0+
PHP7.4+8.1+ recommended for native openssl_pkey_derive
PHP extensionsopenssl with EC supportgmp required as fallback ECDH for PHP < 8.1
HTTPSRequiredService workers only run on secure origins
Chrome67+
Firefox63+
Edge79+
Safari16.4+Push not supported on all Safari versions
ℹ️PHP 8.0 and below: the plugin uses a pure-PHP P-256 scalar multiplication fallback (GMP) for ECDH key agreement. Install the gmp extension if not present.
03 · Installation

Getting started

1
Upload the opwa/ folder to wp-content/plugins/.
2
Activate oPWA in Plugins → Installed Plugins. On activation the plugin: creates three database tables (opwa_subscribers, opwa_campaigns, opwa_analytics), auto-detects the site icon and sets it as the 512px source, generates a default precache list, and sets the VAPID subject to the admin email.
3
Navigate to oPWA in the WordPress admin (top menu bar — the sidebar is hidden on plugin pages).
4
Complete the Manifest tab, generate icons, and configure the Service Worker tab.
5
Generate VAPID keys to enable push notifications.
6
Save and visit your site — the SW will register and the manifest will be served.
04 · File Structure

File structure

opwa/ ├── opwa.php Main plugin file, constants, bootstrap ├── admin/ │ └── class-opwa-admin.php All 9 admin tab views, save handlers, menu ├── assets/ │ ├── css/ │ │ └── opwa-admin-v2.css Admin UI styles (Orravo design system) │ └── js/ │ └── opwa-admin-v2.js Admin JS: AJAX, media picker, charts, route builder └── includes/ ├── class-opwa-analytics.php Summary / chart data, REST beacon/subscribe/unsubscribe ├── class-opwa-cli.php WP-CLI commands ├── class-opwa-core.php Plugin init, manifest/SW serving, AJAX handlers, icon gen ├── class-opwa-db.php Database: tables, CRUD for subscribers/campaigns/analytics ├── class-opwa-push.php VAPID keys, JWT, RFC 8188 encryption, send/send_to_all └── class-opwa-sw-builder.php Dynamic service worker JS generator

Plugin constants

ConstantValue
OPWA_VERSION'1.0.0'
OPWA_PATHAbsolute path to plugin directory
OPWA_URLURL to plugin directory
OPWA_OPTION'opwa_settings' — the wp_options key
OPWA_DB_VERSION'1.0'
05 · Admin Interface

Admin interface

The oPWA admin uses the Orravo 3-row sticky header pattern. The WordPress sidebar is hidden on all plugin pages. Content is full-width.

RowPositionContents
Brand bartop: 32pxoPWA logo, version pill, WP avatar, light/dark toggle
Top navtop: 82pxHorizontal tab links grouped by function
Topbartop: 126pxPage title, save button, secondary actions
5.1 · Dashboard

Dashboard health check

A quick overview of PWA installation status — checks run both in-browser and server-side.

JS-side checks (live in browser)

CheckHow it's tested
HTTPSlocation.protocol === 'https:'
Service Workernavigator.serviceWorker.getRegistration('/')
Manifest linked<link rel="manifest"> present in document head
Installedwindow.matchMedia('(display-mode: standalone)')

PHP-side checklist

  • Icons generated (192px and 512px URLs configured)
  • Offline fallback URL set and the page exists in WordPress
  • VAPID keys generated

The dashboard also shows an iOS instructions card with the share-button steps for iOS Safari users who cannot use the native install prompt.

5.2 · Manifest

Web App manifest

Controls the manifest.webmanifest served at /manifest.webmanifest.

FieldSetting keyNotes
App nameapp_nameUsed as name in manifest
Short nameapp_short_nameUp to 12 chars recommended
Descriptionapp_description
Start URLstart_urlDefaults to /
ScopescopeDefaults to /
Displaydisplaystandalone, fullscreen, minimal-ui, browser
Orientationorientationany, portrait, landscape
Theme colortheme_colorHex — browser UI tint
Background colorbackground_colorSplash screen background
Icon source (512px)icon_512_idAttachment ID for source image
Icon 192px URLicon_192_urlAuto-set after generation
Icon 512px URLicon_512_urlAuto-set after generation
Maskable icon URLicon_maskable_urlAuto-generated with safe zone
Screenshots (×5)screenshots[]src, form_factor, label
Share target enabledshare_targetAdds share_target to manifest
Share target URLshare_target_urlReceives shared content
💡Icon Generator: Upload a single 512×512 PNG/JPG and click Generate All Sizes to create icons at 72, 96, 128, 144, 152, 192, 384, and 512px, plus a maskable variant — all saved in wp-content/uploads/opwa-icons/. Requires the PHP GD extension.
5.3 · Service Worker

Service worker settings

Controls the dynamically-generated SW served at /sw.js.

Global caching strategies

Asset typeSetting keyDefault
HTML pagesstrategy_pagesnetwork-first
Static assets (JS/CSS)strategy_staticcache-first
Imagesstrategy_imagescache-first
Fontsstrategy_fontscache-first

Custom route builder

Add per-URL rules with regex patterns. Each row has:

  • Pattern — JavaScript regex matched against request.url
  • Strategy — one of 5 strategies
  • TTL (seconds) — max age for cached entries (0 = no expiry)
  • Max entries — cap on cache storage entries (0 = unlimited)

Custom routes are serialised to JSON in opwa_settings['custom_routes'].

Preset configurations

PresetDescription
BlogNetwork-first pages, cache-first static/images/fonts
WooCommerceSame as Blog + network-only for checkout/cart/account
PortfolioCache-first everything, long TTLs

Other SW options

OptionKeyDescription
Navigation preloadnavigation_preloadEnables navigationPreload.enable() in SW — avoids startup latency on navigation requests
Background syncbackground_syncEnables IndexedDB form queue
SW versionsw_versionInteger — bump to force cache clear across all clients
5.4 · Offline

Offline experience

FieldKeyDescription
Offline page URLoffline_urlURL served when page is unavailable offline — must exist in WordPress
Offline messageoffline_messageText shown on the offline fallback template
Show logo on offline pageoffline_show_logoRenders the 192px icon
Show cached pages listoffline_show_cachedLists previously cached page titles
Enable form queueoffline_form_queueQueues form submissions via Background Sync API
⚠️The offline URL page must exist in WordPress. The plugin validates this on save and warns if the page is missing.
5.5 · Push Notifications

Push notification settings

Self-hosted Web Push (RFC 8030) with VAPID authentication — no FCM or other third-party service required.

VAPID wizard

1
Generate keys — Clicking Generate VAPID Keys calls OPWA_Push::generate_vapid_keys() which creates an EC P-256 key pair via OpenSSL. The public key (base64url, 65-byte uncompressed point) and private key (PEM) are stored in wp_options.
2
Set subject — Enter a mailto: or https: URI identifying your site, included in the VAPID JWT claim as sub.
⚠️Regenerating keys invalidates all existing subscriber endpoints. Users must re-subscribe.

Push composer

FieldDescription
TitleNotification title
BodyNotification body text
Icon URLSmall notification icon
Image URLLarge hero image (optional)
Click URLWhere the notification takes the user on click
TagNotification tag for deduplication (opwa default)
5.6 · Install Prompt

Install prompt settings

FieldKeyDescription
Banner messagebanner_messageMain install text
Install buttonbanner_buttonCTA button label
Dismiss daysdismiss_daysDays before showing again after dismiss
Show iOS overlayios_overlayAdds "Tap Share → Add to Home Screen" for Safari users

Trigger types

TypeKeyExtra fields
Immediatelyimmediate
After N secondsdelaytrigger_delay (seconds)
After N page viewspageviewstrigger_pageviews
On scrollscrolltrigger_scroll_pct (% page scrolled)
On exit intentexit_intent

Public JS API

JSwindow.opPWA.showBanner();  // Show install banner programmatically
window.opPWA.dismiss();     // Dismiss banner
5.7 · Cache

Live cache management

Live cache inspection and management via a postMessage channel to the active service worker.

JS// Admin JS sends:
reg.active.postMessage({ type: 'OPWA_CACHE_STATS' }, [messageChannel.port2]);

// SW responds with:
{
  'opwa-pages-v1':  { count: 12, size_kb: 340 },
  'opwa-static-v1': { count: 8,  size_kb: 120 },
  // …
}
  • Load Cache Stats — queries the active SW and renders a live breakdown per cache store
  • Clear All Caches — bumps sw_version, causing the SW to delete all old caches on next activate
  • Clear URL — removes a specific URL from the pages cache
5.8 · Analytics

Analytics dashboard

All analytics collected via navigator.sendBeacon to /wp-json/opwa/v1/beacon. No impact on page performance.

Stat cards (all-time)

CardMetric
Push SubscribersCount of active subscriptions
SW Coverage% of page views where SW was active
Cache Hit Ratecache_hits / (cache_hits + cache_misses) × 100
Offline SessionsSessions where network was unavailable
Install PromptsTimes the install banner was shown
InstallsTimes the appinstalled event fired
Install Rateinstalls / prompts × 100
DismissalsTimes the install banner was dismissed

30-day bar charts: SW registrations per day · Installs per day · Page views per day · Cache hits per day.

5.9 · Settings

Plugin settings

Performance

KeyDescription
preload_links<link rel="preload"> for critical assets
navigation_preloadEnable Navigation Preload in SW
lazy_subscribeDelay push subscription prompt

Analytics & Privacy

KeyDefaultDescription
analytics_enabledtrueToggle beacon collection
analytics_retention90Days to keep raw rows

Advanced

KeyDescription
debug_modeAdds console.log statements to SW output
bypass_logged_inSkip SW for logged-in users
woocommerce_modeForces network-only on /checkout, /cart, /my-account
custom_sw_codeAppend raw JS to the generated service worker
06 · Service Worker Caching Strategies

Caching strategies

The SW is generated server-side by OPWA_SW_Builder::build() and served at /sw.js with Content-Type: application/javascript.

StrategyBehaviourBest for
network-firstTry network; fall back to cache on errorHTML pages, dynamic content
cache-firstServe from cache; update in backgroundCSS, JS, fonts, images
stale-while-revalidateServe stale cache immediately; fetch update for next timeFrequently-changing assets where slightly stale is OK
network-onlyNever cache; always require networkCheckout, authentication, payments
cache-onlyOnly serve from cache; never networkPre-cached, locked assets

Cache store names

CacheName pattern
Pagesopwa-pages-v{sw_version}
Static (JS/CSS)opwa-static-v{sw_version}
Imagesopwa-images-v{sw_version}
Offlineopwa-offline-v{sw_version}
Precacheopwa-precache-v{sw_version}

Bumping sw_version causes the old caches to be deleted on the next SW activate event. Each strategy also supports maxAge (seconds) and maxEntries caps.

07 · Web Push Notifications

Self-hosted web push

VAPID (Voluntary Application Server Identification) uses an EC P-256 key pair. All payloads are end-to-end encrypted (AES-128-GCM) — only the subscriber's browser can decrypt.

7.1 · VAPID Key Setup

VAPID key setup

1
GenerateOPWA_Push::generate_vapid_keys() calls openssl_pkey_new(['curve_name' => 'prime256v1']).
2
Public key — The uncompressed 65-byte point (0x04 || X || Y) base64url-encoded. Sent to the browser during subscription via applicationServerKey.
3
Private key — Stored as PEM in wp_options, used to sign the VAPID JWT on every push send.
7.2 · Sending Notifications

Sending notifications

OPWA_Push::send( object $subscriber, array $payload_data, string $vapid_subject )

1
Build VAPID JWT (ES256) — Header {"typ":"JWT","alg":"ES256"}, payload with aud, exp, and sub. Signed with private key via openssl_sign(), DER signature converted to raw R‖S (32 bytes each).
2
Encrypt payloadOPWA_Push::encrypt_payload() — RFC 8188 / ECE aes128gcm.
3
POST to subscriber endpoint with the VAPID auth header.
4
HTTP 410 response → delete subscriber (endpoint expired).
HTTPAuthorization: vapid t={jwt},k={public_key_b64u}
Content-Type: application/octet-stream
Content-Encoding: aes128gcm
TTL: 86400
7.3 · Encryption Details

Payload encryption

Web Push Encryption spec (RFC 8188, ECE draft-03) — AES-128-GCM with ECDH key agreement on P-256.

1
Shared secret — ECDH between an ephemeral sender key pair and the subscriber's p256dh key. PHP 8.1+: openssl_pkey_derive(). PHP < 8.1: pure-PHP double-and-add scalar multiplication using GMP.
2
PRKHKDF-SHA256(salt=auth_secret, ikm=shared_secret, info="WebPush: info\x00" || recv_pub || sender_pub, len=32)
3
CEKHKDF-SHA256(salt=random_16_bytes, ikm=prk, info="Content-Encoding: aes128gcm\x00", len=16)
4
NonceHKDF-SHA256(salt, prk, "Content-Encoding: nonce\x00", len=12)
5
CiphertextAES-128-GCM(key=cek, iv=nonce, plaintext=payload + \x02 + zero_padding)
6
Bodysalt(16) || rs(4, big-endian) || idlen(1) || sender_pub(65) || ciphertext || gcm_tag(16)
08 · Install Prompt

Install prompt lifecycle

Built around the beforeinstallprompt browser event. For iOS Safari (which does not fire this event), the iOS overlay shows share-button instructions instead.

1
Listen for beforeinstallprompt and store the event (deferred).
2
Show the banner according to the configured trigger type.
3
On Install click, calls deferredPrompt.prompt() and waits for user choice.
4
On appinstalled, fires a beacon event to analytics.

Dismiss persistence: Dismissal is stored in localStorage with a timestamp. The banner will not show again until dismiss_days have elapsed.

09 · Offline Experience

Offline experience

When a navigation request fails and the network is unavailable, the SW returns the configured offline URL from the opwa-offline cache.

The offline page can display a custom offline message, the 192px app icon, and a list of cached page titles — the SW reads opwa-pages cache keys and posts them back via postMessage.

10 · Background Sync

Background sync

When background_sync is enabled, the SW intercepts POST form submissions. If the network is unavailable, the request is serialised into IndexedDB (opwa-sync-db, store offline-forms). On reconnect, the SW's sync event replays each queued request to the original form action URL.

Useful for contact forms, newsletter signups, and other POST endpoints that should not be lost when a user goes offline mid-session.

ℹ️Background Sync is supported in Chrome/Edge only. Safari does not yet support the Background Sync API. The SW falls back to a network-online listener in unsupported browsers.
11 · Analytics & Beacons

Analytics & beacons

The front-end script calls navigator.sendBeacon('/wp-json/opwa/v1/beacon', JSON.stringify(payload)) for the following events:

EventFields
page_viewsw_active (bool), url
cache_hiturl, cache_name
cache_missurl
offline_sessionurl
install_prompt_shown
install
install_dismiss
sw_registration

The REST endpoint (OPWA_Analytics::rest_beacon) writes to opwa_analytics, upserting one row per date using INSERT … ON DUPLICATE KEY UPDATE.

12 · WooCommerce Mode

WooCommerce mode

When WooCommerce Mode is on, the service worker applies network-only to requests matching these URL patterns — ensuring cart totals, payment steps, and account pages are never served stale.

  • /checkout and sub-paths
  • /cart and sub-paths
  • /my-account and sub-paths
  • Any URL containing ?wc-ajax=
💡If using a custom WooCommerce checkout URL, add a network-only custom route for it in the Service Worker tab.
13 · Multisite

Multisite support

oPWA supports WordPress Multisite. Each sub-site has its own settings record in its own wp_options. DB tables are created per-site on activation.

BASH# Verify each sub-site after network activation
wp site list --field=url | xargs -I{} wp --url={} opwa status
14 · WP-CLI Commands

WP-CLI commands

All commands use the wp opwa namespace.

wp opwa clear-cache

Bumps sw_version by 1, forcing all clients to delete and re-create caches on next SW activation.

BASHwp opwa clear-cache
# Success: Cache cleared. New SW version: 4

wp opwa send-push

Send a push notification to all subscribers.

BASHwp opwa send-push --title="New post" --body="Check out our latest article" --url="https://site.com/post"
# Success: Done. Sent: 142 / Failed: 3 / Total: 145
FlagRequiredDescription
--titleYesNotification title
--bodyYesNotification body
--urlNoClick-through URL
--iconNoIcon URL

wp opwa list-subscribers

BASHwp opwa list-subscribers
wp opwa list-subscribers --format=json
wp opwa list-subscribers --format=csv
# Columns: ID, Device, User ID, Subscribed, Endpoint (truncated)

wp opwa generate-icons

Generate all PWA icon sizes from a WordPress attachment ID. Must be PNG/JPG at least 512×512.

BASHwp opwa generate-icons 42
# Success: Icons generated: 72x72, 96x96, 128x128, 144x144, 152x152, 192x192, 384x384, 512x512, maskable

wp opwa generate-vapid

BASHwp opwa generate-vapid        # Prompts for confirmation
wp opwa generate-vapid --yes  # Skip confirmation
# Success: VAPID keys generated and saved.
# Public key: BFi2R7...

wp opwa status

Display a summary of current plugin configuration.

BASHwp opwa status
SettingExample value
Plugin version1.0.0
SW version3
App nameMy Site
VAPID configuredYes
Push subscribers142
Pages strategynetwork-first
15 · REST API Endpoints

REST API endpoints

Base namespace: opwa/v1. All three endpoints are public (permission_callback: __return_true) and accept JSON.

POST /wp-json/opwa/v1/beacon

Record an analytics event from the front-end.

JSON// Request body
{
  "event":     "page_view",
  "sw_active": true,
  "url":       "https://site.com/blog/"
}

// Response
{ "ok": true }

POST /wp-json/opwa/v1/subscribe

Register a push subscription endpoint.

JSON// Request body
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/…",
  "keys": { "p256dh": "BN…", "auth": "zq…" }
}

// Response
{ "ok": true, "id": 17 }

POST /wp-json/opwa/v1/unsubscribe

Remove a push subscription.

JSON// Request body
{ "endpoint": "https://fcm.googleapis.com/fcm/send/…" }

// Response
{ "ok": true }
16 · Developer Filters & Actions

Filters & actions

Filters

opwa_manifest_data

Modify the manifest array before it is JSON-encoded and served.

PHPadd_filter( 'opwa_manifest_data', function( array $manifest ): array {
    $manifest['categories'] = [ 'news', 'blog' ];
    $manifest['prefer_related_applications'] = false;
    return $manifest;
} );

opwa_service_worker_routes

Modify or extend the array of custom routes before the SW is generated.

PHPadd_filter( 'opwa_service_worker_routes', function( array $routes ): array {
    $routes[] = [
        'pattern'     => '/api/.*',
        'strategy'    => 'network-only',
        'max_age'     => 0,
        'max_entries' => 0,
    ];
    return $routes;
} );

opwa_sw_extra_code

Append raw JavaScript to the generated service worker.

PHPadd_filter( 'opwa_sw_extra_code', function( string $code ): string {
    $code .= "\nself.addEventListener('message', e => {
    if (e.data === 'MY_MSG') { /* … */ }
});";
    return $code;
} );

opwa_precache_urls

PHPadd_filter( 'opwa_precache_urls', function( array $urls ): array {
    $urls[] = home_url( '/offline-assets/hero.webp' );
    $urls[] = home_url( '/offline-assets/logo.svg' );
    return $urls;
} );

opwa_push_payload

Modify the push notification payload before it is encrypted and sent.

PHPadd_filter( 'opwa_push_payload', function( array $payload, object $subscriber ): array {
    if ( $subscriber->user_id ) {
        $payload['actions'] = [ [ 'action' => 'view', 'title' => 'View now' ] ];
    }
    return $payload;
}, 10, 2 );

opwa_analytics_beacon_data

PHPadd_filter( 'opwa_analytics_beacon_data', function( array $data ): array {
    // Strip URL to path-only for privacy
    if ( isset( $data['url'] ) ) {
        $data['url'] = parse_url( $data['url'], PHP_URL_PATH );
    }
    return $data;
} );

opwa_icon_sizes

PHPadd_filter( 'opwa_icon_sizes', function( array $sizes ): array {
    return array_filter( $sizes, fn( $s ) => in_array( $s, [ 192, 512 ], true ) );
} );

Actions

opwa_after_subscribe

Fires after a new push subscriber is successfully stored.

PHPadd_action( 'opwa_after_subscribe', function( int $subscriber_id, string $endpoint ): void {
    // e.g., send a welcome notification
}, 10, 2 );

opwa_after_push_send

Fires after a push campaign completes.

PHPadd_action( 'opwa_after_push_send', function( array $result, array $payload ): void {
    // $result = [ 'sent' => 140, 'failed' => 2, 'total' => 142 ]
    error_log( 'Push sent: ' . wp_json_encode( $result ) );
}, 10, 2 );

opwa_after_cache_clear

PHPadd_action( 'opwa_after_cache_clear', function( int $new_version ): void {
    do_action( 'my_plugin_purge_cdn' );
} );

opwa_on_activate

Fires after the plugin runs its full activation routine (tables created, defaults set).

PHPadd_action( 'opwa_on_activate', function(): void {
    // One-time setup for your own plugin
} );
17 · Database Schema

Database schema

opwa_subscribers

ColumnTypeDescription
idBIGINT UNSIGNED AUTO_INCREMENTPrimary key
endpointTEXT NOT NULLPush subscription URL
p256dhTEXT NOT NULLClient public key
authVARCHAR(255)Auth secret
user_idBIGINT UNSIGNED NULLWP user ID if logged in
device_typeVARCHAR(20)mobile, tablet, desktop
user_agentTEXTRaw UA string
created_atDATETIMESubscription time

Unique index on MD5 hash of endpoint to prevent duplicates.

opwa_campaigns

ColumnTypeDescription
idBIGINT UNSIGNED AUTO_INCREMENTPrimary key
titleVARCHAR(255)Notification title
bodyTEXTNotification body
icon_urlVARCHAR(1000)Icon URL
click_urlVARCHAR(1000)Click-through URL
sentINTSuccessful deliveries
failedINTFailed deliveries
totalINTTotal subscribers at send time
created_atDATETIMESend time

opwa_analytics

One row per calendar date. Uses INSERT … ON DUPLICATE KEY UPDATE so concurrent requests safely increment counters.

ColumnTypeDescription
idBIGINT UNSIGNED AUTO_INCREMENTPrimary key
stat_dateDATE NOT NULLDate (YYYY-MM-DD) — unique key
page_views_totalINTTotal page view beacons
page_views_swINTPage views where SW was active
cache_hitsINTCache-served responses
cache_missesINTNetwork-fallback responses
offline_sessionsINTSessions without network
install_prompts_shownINTInstall banner impressions
installsINTappinstalled events
install_dismissalsINTBanner dismissals
sw_registrationsINTSW registration events
18 · Security

Security model

Nonce protection

All admin AJAX endpoints verify wp_verify_nonce( $nonce, 'opwa_admin' ) and check current_user_can('manage_options') before processing.

Input sanitisation

Data typeFunction used
URLsesc_url_raw()
Textsanitize_text_field()
HTML fieldswp_kses_post()
Integersintval()
Booleans(bool) cast

VAPID private key storage

The VAPID private key PEM is stored in wp_options. Access requires the manage_options capability. If your site has untrusted admin users, consider encrypting the PEM at rest.

Push payload security

All push payloads are end-to-end encrypted (AES-128-GCM) before transmission. Only the subscriber's browser can decrypt the payload. The beacon endpoint writes aggregated counts only — no raw URL storage.

ℹ️If your site uses a strict CSP, allow 'self' for worker-src (for the SW) and connect-src for the beacon REST endpoint.
19 · Performance Considerations

Performance notes

TopicGuidance
SW script size~8–15 KB before gzip. Keep custom routes minimal.
Precache listLarge lists slow SW install. Limit to 20–30 critical URLs.
Push payload sizeWeb Push limits payloads to 4 KB including encryption overhead. Keep notification bodies under 1 KB.
Analytics beaconUses navigator.sendBeacon — non-blocking, no impact on page performance.
Navigation PreloadEnable navigation_preload to avoid SW startup latency on navigation requests.
Icon generationGD-based icon generation is a one-time operation. Generated PNGs cached in uploads/opwa-icons/.
20 · Troubleshooting

Common issues

Service worker not registering

  • Verify the site is served over HTTPS (or localhost).
  • Check browser DevTools → Application → Service Workers for errors.
  • Ensure no other plugin intercepts requests to /sw.js. The SW is served at init priority 1 via WordPress, not as a real file.
  • If using a CDN, ensure /sw.js and /manifest.webmanifest are excluded from the CDN cache.

Push notifications not received

1
Confirm VAPID keys are generated — Settings → VAPID Wizard step 1 shows a public key.
2
Check the subscriber table is not empty — users must visit the site and accept the permission prompt.
3
Test with the Send Test Push button.
4
Check browser DevTools → Application → Push Messaging for errors.
5
HTTP 410 from the push service means the subscription is expired — the plugin deletes it automatically.
6
Firefox requires --allow-feature=web-push in some testing environments.

Icons not generating

  • Verify the PHP GD extension is active: php -m | grep gd
  • The source attachment must be at least 512×512 pixels.
  • The wp-content/uploads/opwa-icons/ directory must be writable.

ECDH / encryption errors

  • PHP < 8.1 requires the GMP extension for the P-256 fallback: php -m | grep gmp
  • PHP 8.1+ uses openssl_pkey_derive() natively.
  • OpenSSL must be compiled with EC curve support: openssl ecparam -list_curves | grep prime256v1

Other issues

IssueFix
Manifest not linkingCheck another plugin isn't outputting a <link rel="manifest"> pointing elsewhere. The plugin hooks into wp_head — ensure your theme doesn't remove it.
Background sync not firingOnly supported in Chromium. Verify background_sync is enabled. Check SW DevTools → Background Sync for queued tags.
WooCommerce checkout issuesEnable WooCommerce Mode in Settings. For custom checkout URLs, add a network-only custom route.
✦ Need help?

Got a question about oPWA?

Reach out directly — Kenneth replies within 24 hours.