
Push Notifications
- 66 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Send browser web-push notifications for price drops, back-in-stock alerts, and cart reminders to re-engage shoppers without their email.
About
Configures web push subscriber management and triggering for back-in-stock, price-drop, and abandoned-cart messages via apps like PushOwl or OneSignal. A developer uses it to add a browser re-engagement channel alongside email.
- Back-in-stock, price-drop, and cart-reminder push triggers
- Opt-in prompt timing drives subscriber capture without custom service-worker code
Push Notifications by the numbers
- 66 all-time installs (skills.sh)
- Ranked #522 of 853 Sales & Marketing skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill push-notificationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 66 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Send browser web-push notifications for price drops, back-in-stock alerts, and cart reminders to re-engage shoppers without their email.
Files
Push Notifications
Overview
Web push notifications deliver timely messages to subscribers even when they are not on your site — for back-in-stock alerts, price drops, and cart reminders. Push requires explicit browser permission, making the opt-in prompt timing critical. For Shopify, WooCommerce, and BigCommerce, dedicated push notification apps (PushOwl, OneSignal) handle all the subscriber management, triggering logic, and delivery without custom service worker code.
When to Use This Skill
- When adding back-in-stock notifications to replace static "notify me" email forms
- When recovering abandoned carts via a browser push channel alongside email
- When building a price-watch feature for wishlisted items
- When email deliverability is poor and a supplemental channel is needed
- When targeting mobile-first markets where push opt-in rates exceed email opt-in
Core Instructions
Step 1: Choose the right push notification platform
| Platform | Best For | Shopify | WooCommerce | BigCommerce | Price |
|---|---|---|---|---|---|
| PushOwl | Shopify-native, back-in-stock + abandonment | App Store | — | — | Free tier; $19+/mo |
| OneSignal | All platforms, free tier, highly configurable | Via JS tag | Plugin | Via JS tag | Free tier; $9+/mo |
| Klaviyo Web Push | Already using Klaviyo for email | App Store | Plugin | App Marketplace | Included in Klaviyo |
| PushEngage | WooCommerce + segmented campaigns | — | Plugin | Via JS tag | Free tier; $9+/mo |
Shopify recommendation: Use PushOwl — it's the most integrated Shopify push app with built-in back-in-stock, cart abandonment, and shipping alerts.
WooCommerce recommendation: Use PushEngage or OneSignal — both have WordPress plugins and handle subscriber management automatically.
Step 2: Set up push notifications
---
Shopify with PushOwl
1. Install PushOwl from the Shopify App Store 2. Go to PushOwl → Settings → Opt-in Prompt and configure:
- Delay the prompt: set it to trigger after a customer views 2+ pages or adds an item to cart
- Opt-in message: "Get notified when items are back in stock and for price drops"
3. Go to PushOwl → Automations → Back in Stock and enable it — PushOwl automatically adds a "Notify Me" button to out-of-stock products and fires the push when inventory is replenished 4. Go to PushOwl → Automations → Cart Abandonment and enable it:
- Set timing: 1 hour after abandonment, then 24 hours
- Customize the notification message and the cart recovery URL
5. Go to PushOwl → Campaigns to send broadcast push notifications for sales, new arrivals, or flash discounts
---
WooCommerce with PushEngage
1. Install PushEngage from the WordPress plugin directory (free tier available) 2. Go to PushEngage → Settings → Subscription Prompt and configure the opt-in dialog 3. Enable automated campaigns:
- Go to PushEngage → Automation → Cart Abandonment and set the timing and message
- Go to PushEngage → Automation → Back in Stock and enable it (requires WooCommerce stock event integration)
4. For price drop alerts: go to PushEngage → Automation → Price Drop Alert and enable subscriber opt-in per product 5. Use PushEngage → Broadcast to send manual push campaigns to all subscribers
---
BigCommerce with OneSignal
1. Sign up for OneSignal at onesignal.com and create a Web Push app 2. Go to OneSignal → Settings → Web Push → Setup and follow the HTTPS domain verification 3. Add the OneSignal JavaScript snippet to your BigCommerce store via Storefront → Script Manager:
<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
<script>
window.OneSignalDeferred = window.OneSignalDeferred || [];
OneSignalDeferred.push(async function(OneSignal) {
await OneSignal.init({ appId: "YOUR_APP_ID" });
});
</script>4. For back-in-stock: use BigCommerce webhooks to trigger a OneSignal API call when product stock transitions from 0 to available 5. For cart abandonment: use BigCommerce's Abandoned Cart webhook + OneSignal REST API to send cart recovery pushes
---
Custom / Headless
For headless stores, implement push using the Web Push API directly:
// Service worker — save as /sw.js in your public directory
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon ?? '/icons/icon-192.png',
image: data.image,
data: { url: data.url },
actions: data.actions ?? [],
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data?.url ?? '/'));
});// Server-side push sending using the web-push library
import webpush from 'web-push';
webpush.setVapidDetails(
'mailto:admin@yourstore.com',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
// Generate VAPID keys once: npx web-push generate-vapid-keys
async function sendPushNotification(subscription: PushSubscription, payload: {
title: string;
body: string;
url: string;
icon?: string;
}) {
try {
await webpush.sendNotification(subscription, JSON.stringify(payload));
} catch (err: any) {
if (err.statusCode === 410) {
// Subscription expired — remove from database
await db.pushSubscriptions.deleteByEndpoint(subscription.endpoint);
}
}
}
// Triggered when inventory transitions from 0 to > 0
async function notifyBackInStock(productId: string) {
const product = await db.products.findById(productId);
const waitlist = await db.pushWaitlist.findByProduct(productId);
for (const entry of waitlist) {
const sub = await db.pushSubscriptions.findByUserId(entry.userId);
if (!sub) continue;
await sendPushNotification(sub, {
title: 'Back in stock!',
body: `${product.name} is available again`,
url: `${process.env.STORE_URL}/products/${product.slug}`,
icon: product.images[0]?.url,
});
}
}Step 3: Optimize opt-in timing
The most critical factor for push notification effectiveness is when you show the permission prompt. Never ask on the first page load.
Best triggers for the opt-in prompt:
- After the visitor has viewed 3+ products
- Immediately after a customer adds an item to cart
- When a customer views an out-of-stock product (context: "Get notified when it's back")
- On the order confirmation page ("Get shipping updates and deals via browser push")
In PushOwl: go to Settings → Opt-in Prompt → Advanced and set the trigger condition. In PushEngage: go to Subscription Prompt → Display Rules and set page view count or cart event triggers.
Step 4: Set up the highest-converting push campaigns
Priority order by conversion rate:
1. Back-in-stock alerts — highest CTR (15–25%); customers opted in specifically for this product 2. Cart abandonment — set for 1 hour and 24 hours after abandonment; use urgency in the second push ("Your cart expires soon") 3. Price drop alerts — customers watching a specific product convert at 10–20% when notified of their target price 4. Shipping updates — low-friction way to grow push subscribers (capture at order confirmation); keeps brand top-of-mind
In PushOwl: all four are available as pre-built automations under Automations — enable them and customize the message.
Step 5: Measure push performance
| Metric | Healthy Target | Where to Find |
|---|---|---|
| Opt-in rate | 5–15% of new visitors | App dashboard |
| Back-in-stock click rate | 15–25% | PushOwl/PushEngage analytics |
| Cart abandonment recovery rate | 2–5% | App analytics |
| Unsubscribe rate per campaign | < 2% | App analytics |
If unsubscribe rate is above 2%, reduce push frequency or improve message relevance.
Best Practices
- Never request permission on first page load — acceptance rates jump from ~5% to ~25% when shown after a user action like adding to cart
- Limit to 2 push notifications per day per user maximum — excessive frequency is the top driver of opt-out
- Set a TTL on time-sensitive notifications — flash sale pushes should expire when the sale ends; PushOwl and PushEngage both support notification expiry
- Keep notification body under 100 characters — longer bodies are truncated on Android; test on mobile before deploying
- Use "Notify me" at the product level for out-of-stock — this captures high-intent subscribers with context; generic sitewide opt-ins perform worse
Common Pitfalls
| Problem | Solution |
|---|---|
| Low opt-in rate | Move the permission prompt to after add-to-cart or product view #3; never show on first load |
| iOS users not receiving push | Web push on iOS requires iOS 16.4+ and the user must install the site as a PWA (Add to Home Screen); this is a browser limitation |
| Push notifications not firing for back-in-stock | Verify the inventory webhook is connected in PushOwl/PushEngage settings; check the app's automation logs |
| High unsubscribe rate | Reduce push frequency; add preference management so subscribers can choose which notification types they receive |
| Duplicate subscriptions in the database | For custom implementations, use upsert keyed on (userId, endpoint) |
Related Skills
- @email-marketing-automation
- @cart-abandonment-recovery
- @sms-marketing
- @exit-intent-popups
- @customer-retention-engine
{
"context": "Tests whether the agent implements the smart opt-in timing strategy (gated behind user engagement, not first page load), persists opt-in state with localStorage, sets TTL on the flash sale broadcast, and documents the 2-per-day frequency limit and other best practices.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Permission NOT on page load",
"max_score": 10,
"description": "client/push-optin.ts does NOT call Notification.requestPermission() immediately on script load or DOMContentLoaded — it is gated behind a user interaction or engagement check"
},
{
"name": "Page view threshold",
"max_score": 8,
"description": "client/push-optin.ts tracks page view count (e.g., using sessionStorage) and only proceeds after reaching a minimum threshold of 3 or more page views"
},
{
"name": "Add-to-cart trigger",
"max_score": 10,
"description": "client/push-optin.ts triggers the permission request in response to an add-to-cart event (e.g., listens for 'cart:item:added' or similar cart event)"
},
{
"name": "Dismissed state in localStorage",
"max_score": 8,
"description": "client/push-optin.ts stores a 'pushDismissed' (or equivalent) flag in localStorage when the user declines the prompt"
},
{
"name": "Subscribed state in localStorage",
"max_score": 8,
"description": "client/push-optin.ts stores a 'pushSubscribed' (or equivalent) flag in localStorage when the user successfully subscribes"
},
{
"name": "Guard against re-prompting",
"max_score": 8,
"description": "client/push-optin.ts returns early (no dialog shown) if the user has already subscribed OR already dismissed the prompt — checking both localStorage flags"
},
{
"name": "Flash sale TTL set",
"max_score": 10,
"description": "server/broadcast-flash-sale.ts passes a TTL option to webpush.sendNotification() calculated from the sale's end time (so expired-sale pushes are not delivered)"
},
{
"name": "410 cleanup in broadcast",
"max_score": 8,
"description": "server/broadcast-flash-sale.ts collects or identifies subscriptions that return a 410 status and deletes them from the database after the broadcast"
},
{
"name": "Promise.allSettled usage",
"max_score": 8,
"description": "server/broadcast-flash-sale.ts uses Promise.allSettled() (not Promise.all()) to send notifications in parallel without aborting on individual failures"
},
{
"name": "Daily frequency limit documented",
"max_score": 8,
"description": "DESIGN.md mentions a limit of no more than 2 push notifications per day per user as a best practice or constraint"
},
{
"name": "Body length constraint documented",
"max_score": 6,
"description": "DESIGN.md mentions keeping notification body text under 100 characters to avoid truncation on Android"
},
{
"name": "Cart reminder actions",
"max_score": 8,
"description": "DESIGN.md or broadcast-flash-sale.ts mentions or uses the 'actions' field in notification payloads to provide binary choices to users"
}
]
}
Push Notification Opt-In Flow and Flash Sale Broadcast
Problem/Feature Description
NorthShore Goods has the core push infrastructure running but is seeing very low opt-in rates from visitors. The product team believes the permission dialog is being shown too aggressively. Additionally, the marketing team wants to send a flash sale notification to all subscribers during a one-day sale event — but needs the delivery window to respect the sale's end time so customers don't receive a stale "sale is on" notification after it has ended.
Your task is to write two pieces of JavaScript/TypeScript:
1. A smart opt-in manager that decides when to show the browser push permission dialog. The team wants to avoid asking cold visitors and instead wait until customers have shown real engagement with the store. User preferences (whether they've subscribed or dismissed the prompt) must survive page refreshes.
2. A server-side broadcast function that sends a flash sale notification to all subscribers, cleans up any expired subscriptions discovered during the send, and ensures the notification is not delivered after the sale has ended.
Output Specification
Produce the following files:
1. client/push-optin.ts — Client-side TypeScript implementing the smart opt-in prompt. The function should track engagement state and only ask the user for permission at the right moment. It should also avoid showing the dialog again to users who have already subscribed or dismissed it.
2. server/broadcast-flash-sale.ts — Server-side TypeScript function broadcastFlashSale(saleDetails) that sends a flash sale push to all subscribers, with appropriate expiry handling. The function should accept at minimum: title, discountPct, endsAt (Date), and url.
3. DESIGN.md — A brief document explaining the opt-in strategy chosen and any important constraints or best practices that guided the implementation decisions (e.g., sending frequency limits, notification body length, etc.).
{
"context": "Tests whether the agent correctly implements server-side push subscription storage with upsert logic, back-in-stock notification sending with 410 cleanup, price drop notification with watch deactivation, and proper error handling patterns.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Upsert on userId+endpoint",
"max_score": 10,
"description": "api/push-subscribe.ts uses an upsert operation keyed on both userId AND endpoint (not a plain insert that could create duplicates)"
},
{
"name": "Stores p256dh and auth",
"max_score": 8,
"description": "api/push-subscribe.ts stores both subscription.keys.p256dh and subscription.keys.auth fields from the subscription object"
},
{
"name": "Back-in-stock title/body",
"max_score": 8,
"description": "api/notify-back-in-stock.ts sends a notification with title 'Back in stock!' and body containing the product name and text 'is available again'"
},
{
"name": "Back-in-stock Buy Now action",
"max_score": 8,
"description": "api/notify-back-in-stock.ts includes an actions array with a 'Buy Now' action entry in the notification payload"
},
{
"name": "410 error cleanup (back-in-stock)",
"max_score": 10,
"description": "api/notify-back-in-stock.ts catches errors from webpush.sendNotification and deletes the subscription when the error statusCode is 410"
},
{
"name": "Try/catch with status logging",
"max_score": 8,
"description": "At least one of the notification files wraps webpush.sendNotification() in a try/catch and logs or uses the error's status code"
},
{
"name": "Price drop threshold check",
"max_score": 8,
"description": "api/notify-price-drop.ts compares newPriceCents against the watch's target price and only sends a notification when the new price is at or below the target"
},
{
"name": "Price shown in body",
"max_score": 8,
"description": "api/notify-price-drop.ts includes the actual new price amount (converted from cents to dollars, e.g. toFixed(2)) in the notification body"
},
{
"name": "Price watch deactivated",
"max_score": 10,
"description": "api/notify-price-drop.ts deactivates or marks as inactive the price watch record after sending the notification (one-shot: not re-triggered on future price checks)"
},
{
"name": "Notification body length",
"max_score": 8,
"description": "All notification body strings in both notify files are under 100 characters in length (or use template strings that, with realistic product names, would remain under 100 characters)"
},
{
"name": "Product URL in payload",
"max_score": 8,
"description": "Both notify files include a url field in the notification payload pointing to the product page (using the product's slug or id)"
},
{
"name": "410 cleanup (price drop)",
"max_score": 6,
"description": "api/notify-price-drop.ts also handles 410 errors by removing or skipping expired subscriptions"
}
]
}
Server-Side Push Notification Handlers
Problem/Feature Description
NorthShore Goods has the push subscription infrastructure in place and now needs the server-side logic to complete two critical notification flows. First, the team needs an endpoint to receive and persistently store browser push subscriptions from customers. Second, they need the back-end logic that fires notifications to subscribed customers when a product they are waiting on comes back into stock.
The store runs a PostgreSQL-backed database with tables for pushSubscriptions, pushWaitlist, and products. The back-end is a TypeScript/Node.js API using Express. A webpush instance has already been configured with VAPID keys elsewhere and is available as an import. Your code should be production-quality and handle real-world edge cases gracefully.
Output Specification
Produce the following files:
1. api/push-subscribe.ts — The POST handler for /api/push/subscribe that saves a push subscription to the database. The handler should handle the case where a browser re-subscribes or the same user subscribes from multiple devices gracefully.
2. api/notify-back-in-stock.ts — The function notifyBackInStock(productId: string) that is triggered when a product's inventory transitions from 0 to greater than 0. It should look up the waitlisted subscribers and send each a push notification.
3. api/notify-price-drop.ts — The function checkPriceWatches(productId: string, newPriceCents: number) that fires notifications when a product's new price meets a customer's target price.
The files should use TypeScript and assume the db and webpush objects are available as imports. Include realistic pseudo-implementations of any db helper calls.
{
"context": "Tests whether the agent correctly sets up the web-push VAPID infrastructure, implements the service worker with proper push and notificationclick handlers, and writes the client-side subscription code with required safety checks and helper functions.",
"type": "weighted_checklist",
"checklist": [
{
"name": "web-push package",
"max_score": 8,
"description": "server/push-config.ts imports or references the 'web-push' package (not an alternative like 'push.js' or 'pushpad')"
},
{
"name": "VAPID key generation CLI",
"max_score": 8,
"description": "README.md or push-config.ts includes the command 'npx web-push generate-vapid-keys' (or equivalent reference to this specific command)"
},
{
"name": "setVapidDetails call",
"max_score": 8,
"description": "server/push-config.ts calls webpush.setVapidDetails() with three arguments: a mailto: email string, a public key env var, and a private key env var"
},
{
"name": "VAPID keys from env vars",
"max_score": 8,
"description": "The VAPID public and private keys in server/push-config.ts are read from environment variables (e.g., process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY), not hardcoded"
},
{
"name": "SW path at /sw.js",
"max_score": 8,
"description": "client/subscribe.ts registers the service worker using the path '/sw.js' (not '/service-worker.js' or any other path)"
},
{
"name": "Browser compatibility check",
"max_score": 8,
"description": "client/subscribe.ts checks for both 'serviceWorker' in navigator AND 'PushManager' in window before proceeding"
},
{
"name": "userVisibleOnly flag",
"max_score": 8,
"description": "client/subscribe.ts passes userVisibleOnly: true in the pushManager.subscribe() options object"
},
{
"name": "urlBase64ToUint8Array helper",
"max_score": 8,
"description": "client/subscribe.ts includes or uses a helper function to convert the base64 VAPID public key to a Uint8Array for applicationServerKey"
},
{
"name": "SW push event handler",
"max_score": 8,
"description": "public/sw.js contains a 'push' event listener that calls self.registration.showNotification() with at least title, body, icon, and data fields"
},
{
"name": "SW badge path",
"max_score": 6,
"description": "public/sw.js sets the badge property to '/icons/badge-72.png' in the showNotification() options"
},
{
"name": "SW notificationclick handler",
"max_score": 8,
"description": "public/sw.js contains a 'notificationclick' event listener that calls event.notification.close() and opens the URL from notification data using clients.openWindow()"
},
{
"name": "iOS platform limitation",
"max_score": 8,
"description": "README.md mentions that iOS Safari requires iOS 16.4+ AND that the site must be added to the Home Screen (PWA install) for push to work"
},
{
"name": "Subscription sent to server",
"max_score": 6,
"description": "client/subscribe.ts sends the subscription to the server (e.g., via fetch POST) including subscription.toJSON() or equivalent serialization"
}
]
}
Push Notification Infrastructure Setup
Problem/Feature Description
You are a developer at a mid-sized e-commerce company called NorthShore Goods. The marketing team has just approved a new initiative to add browser push notifications to the store. Customers should be able to opt in and receive timely alerts. The engineering team has decided to implement this using the standard Web Push API with a Node.js/TypeScript backend.
Your job is to lay the foundational infrastructure: the server-side push configuration, the browser-side service worker that receives and displays notifications, and the client-side subscription code that registers users. The code should be production-ready — handling edge cases and browser compatibility checks appropriately.
Output Specification
Produce the following files:
1. server/push-config.ts — Server-side configuration code that initializes the push notification library using credentials from environment variables. Include a comment showing the CLI command used to generate the credentials.
2. public/sw.js — The service worker file that handles incoming push events and notification click interactions.
3. client/subscribe.ts — Client-side TypeScript that registers the service worker, requests notification permission, subscribes the browser to push, and sends the subscription to the server. Include any required helper functions.
4. README.md — A brief setup guide explaining the steps to initialize the system (key generation, environment variables, deployment notes), including any important platform limitations developers should be aware of.
{
"name": "finsi/push-notifications",
"version": "0.1.0",
"summary": "Web push for price drops, back-in-stock, and cart reminders",
"skills": {
"push-notifications": {
"path": "SKILL.md"
}
}
}