Full-stack web push notification system with two backend options (Node.js / Python) and a React testing dashboard.
βββββββββββββββββββββββ βββββββββββββββββββββββ
β React Frontend β β React Frontend β
β (User A browser) β β (User B browser) β
ββββββββββ¬βββββββββββββ ββββββββββ¬βββββββββββββ
β subscription β subscription
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Backend API β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Node.js (Express)β β Python (FastAPI) β β
β β :4000 β β :8001 β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββββββββββ β
β β β β
β βββββββββββ¬ββββββββββββ β
β βΌ β
β Azure Notification Hubs β
β (installations + send) β
β β β
β βΌ β
β Browser Push (FCM/APNs) β
β β β
β βΌ β
β Service Worker β Notification β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
p-not/
βββ node-backend/ # Node.js backend (Express)
β βββ src/
β β βββ index.js # Server entry
β β βββ config.js # Environment config
β β βββ routes.js # API routes
β β βββ notificationService.js # Azure NH client wrapper
β β βββ generate-vapid.js # VAPID key generator
β βββ .env.example
β βββ package.json
β
βββ python-backend/ # Python backend (FastAPI)
β βββ app/
β β βββ main.py # FastAPI app + middleware + logging
β β βββ config.py # Pydantic settings
β β βββ auth.py # HMAC-SHA256 + SAS token generation
β β βββ client.py # NotificationHubsClient (REST API)
β β βββ routes.py # All API endpoints
β βββ requirements.txt
β βββ .env
β
βββ frontend/ # React + Vite
β βββ src/
β β βββ App.jsx # Dashboard with scenario cards
β β βββ main.jsx # Entry point
β β βββ usePushNotifications.js # Push hook (all API calls)
β β βββ components/
β β βββ Modal.jsx # Base modal + form fields
β β βββ SubscribeModal.jsx # Subscribe as User A/B
β β βββ SendBasicModal.jsx # Phase 2.1 β tag-based send
β β βββ SendRichModal.jsx # Phase 3 β icon, image, actions
β β βββ SendTargetedModal.jsx # Phase 2.2, 4.3, 4.4 β TTL, urgency
β β βββ BroadcastModal.jsx # Phase 2.3 β all subscribers
β β βββ ScheduledModal.jsx # Phase 7 β future delivery
β β βββ DebugPanel.jsx # Real-time debug log
β βββ public/
β β βββ sw.js # Service worker (rich notification support)
β βββ index.html
β βββ vite.config.js
β
βββ package.json # Root scripts
βββ .gitignore
βββ README.md
- Node.js 18+
- Python 3.12+ (for Python backend)
- Azure Notification Hub (Free tier works)
- Modern browser (Chrome, Firefox, Edge β HTTPS or localhost)
cd node-backend
npm install
npm run generate-vapidCopy the output β you'll need both keys.
- Create a Notification Hub in Azure Portal
- Go to Notification Hub β Access Policies β copy
DefaultFullSharedAccessSignatureconnection string - Go to Browser Push (Web) β set:
- Public Key: from step 1
- Private Key: from step 1
- Subject:
mailto:you@example.com
Node.js backend (node-backend/.env):
AZURE_NOTIFICATION_HUB_CONNECTION_STRING=Endpoint=sb://<namespace>.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<key>;EntityPath=<hub-name>
AZURE_NOTIFICATION_HUB_NAME=<hub-name>
VAPID_PUBLIC_KEY=<from step 1>
VAPID_PRIVATE_KEY=<from step 1>
VAPID_SUBJECT=mailto:you@example.com
PORT=4000
FRONTEND_URL=http://localhost:5173Python backend (python-backend/.env):
AZURE_NOTIFICATION_HUB_CONNECTION_STRING=Endpoint=sb://<namespace>.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<key>
AZURE_NOTIFICATION_HUB_NAME=<hub-name>
VAPID_PUBLIC_KEY=<from step 1>
VAPID_PRIVATE_KEY=<from step 1>
VAPID_SUBJECT=mailto:you@example.com
PORT=8001
FRONTEND_URL=http://localhost:5173# Node backend
cd node-backend && npm install
# Python backend
cd python-backend && pip install -r requirements.txt
# Frontend
cd frontend && npm installOption A β Python backend + React frontend:
# Terminal 1
cd python-backend
python -m uvicorn app.main:app --reload --port 8001
# Terminal 2
cd frontend
npm run devOption B β Node backend + React frontend:
# Terminal 1
cd node-backend
npm run dev # port 4000
# Terminal 2
cd frontend
npm run dev # port 5173If using Node backend, update
API_BASEinfrontend/src/usePushNotifications.jstohttp://localhost:4000/api.
- Open
http://localhost:5173in Chrome - Click Subscribe β enter
user-aβ subscribe - Open
http://localhost:5173in a second browser/incognito - Click Subscribe β enter
user-bβ subscribe - Use the dashboard scenario cards to test each feature
| Phase | Scenario | Description |
|---|---|---|
| Core | Subscribe | Register browser as User A or User B |
| 2.1 | Send to User | Tag-based send to user:user-a |
| 2.2 | Tag Expression | Multi-tag filtering (user:user-a AND web) |
| 2.3 | Broadcast | Send to ALL subscribers via all tag |
| 3.x | Rich Notification | Custom icon, image, action buttons, tag collapse |
| 4.3 | TTL | Notification expires after N seconds |
| 4.4 | Urgency | Priority levels: low / normal / high |
| 4.1 | 410 GONE | Auto-cleanup of expired subscriptions |
| 5.1 | Registrations | List all Azure installations (debug) |
| 7.1 | Schedule | Future delivery (Standard SKU only) |
| 7.2 | Cancel | Cancel a scheduled notification |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/vapid-public-key |
Returns VAPID public key for browser subscription |
POST |
/api/save-subscription |
Register browser push subscription as Azure installation |
DELETE |
/api/unsubscribe |
Remove installation from Azure |
| Method | Endpoint | Body | Description |
|---|---|---|---|
POST |
/api/send |
{ userId, title, body, url? } |
Send to specific user |
POST |
/api/send/rich |
{ userId, title, body, icon?, image?, actions?, tag?, silent? } |
Rich notification |
POST |
/api/send/targeted |
{ tagExpression, title, body, ttl?, urgency? } |
Send with tag expression + TTL + urgency |
POST |
/api/broadcast |
{ title, body, url?, ttl?, urgency? } |
Send to all subscribers |
POST |
/api/send/scheduled |
{ userId, title, body, scheduledTime } |
Schedule future delivery (Standard SKU) |
DELETE |
/api/scheduled/{id} |
β | Cancel scheduled notification |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/registrations |
List all installations (debug) |
GET |
/api/installations/{id} |
Get specific installation |
GET |
/api/outcome/{id} |
Get delivery outcome for a sent notification |
GET |
/api/feedback |
Get PNS feedback container URL |
The service worker (frontend/public/sw.js) handles:
- Push events β displays notification with title, body, icon, image, badge, actions
- Click events β opens the
urlfrom the notification payload - Tag collapse β notifications with same
tagreplace previous ones - Silent mode β
silent: truesuppresses sound/vibration
When a user subscribes:
- Browser generates a push subscription (endpoint + keys)
- Frontend sends it to the backend
- Backend creates an installation in Azure with:
installationIdβ UUIDplatformβ"browser"pushChannelβ endpoint, p256dh, authtagsβ[user:<id>, all, web]expirationTimeβ 30 days
When sending a notification:
- Backend calls Azure REST API (
POST /messages) - Azure matches tags to installations
- Azure forwards to browser push service (FCM)
- FCM delivers to the service worker
Tags enable targeting:
| Tag | Purpose |
|---|---|
user:<id> |
Target a specific user |
all |
Target everyone (broadcast) |
web |
Target web browsers only |
Tag expressions: user:user-a, user:user-a AND web, all.
- HTTPS β push requires HTTPS (localhost is exempt)
- Auth middleware β replace
userIdfrom request body with JWT/session - Connection string security β use
DefaultSendSharedAccessSignature(notFull) - 410 handling β auto-delete expired installations
- Rate limiting β prevent abuse of send endpoints
- TTL β set appropriate expiration for time-sensitive notifications
- Error monitoring β log Azure NH errors with tracking IDs
- Registration cleanup β periodically remove expired installations
| Problem | Cause | Fix |
|---|---|---|
401 Unauthorized |
Wrong connection string | Add EntityPath=<hub-name> to connection string |
404 Hub not found |
Wrong hub name | Check AZURE_NOTIFICATION_HUB_NAME matches Azure Portal |
400 Bad Request |
Invalid JSON body | Check request Content-Type is application/json |
| Notification not received | VAPID not configured | Set VAPID keys in Azure Portal β Browser Push |
successCount: 0 |
No matching installations | Check tags match between installation and send |
| Permission denied | User blocked notifications | Detect via Notification.permission === "denied" |
| Layer | Technology |
|---|---|
| Frontend | React 18, Vite, Service Workers |
| Node backend | Express, @azure/notification-hubs v2 |
| Python backend | FastAPI, httpx, Azure REST API |
| Push service | Azure Notification Hubs |
| Browser push | FCM (Chrome/Firefox/Edge), APNs (Safari) |
MIT