Self-Hosted Push Notifications Specification
Part 8: Complete Reference & FAQ
Version: 1.0
Last Updated: October 2025
Prerequisites: Part 7: Best Practices, Security & Optimization
Author: Bunty9
License: MIT (Free to use and adapt)
Table of Contents
- Complete API Reference
- Browser Compatibility Matrix
- Frequently Asked Questions (FAQ)
- Troubleshooting Guide
- Migration Guides
- Glossary of Terms
- Additional Resources
- Specification Summary
Complete API Reference
Backend Endpoints
POST /api/push/subscribe
Subscribe to push notifications.
Request:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "BGt...",
"auth": "abc..."
},
"userAgent": "Mozilla/5.0..."
}
Response:
{
"success": true,
"message": "Subscribed successfully",
"data": {
"subscriptionId": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": "web-abc123",
"deviceName": "Chrome on macOS"
}
}
Status Codes:
-
200 OK
- Subscription successful -
400 Bad Request
- Invalid request body -
401 Unauthorized
- Authentication required -
429 Too Many Requests
- Rate limit exceeded
POST /api/push/unsubscribe
Unsubscribe from push notifications.
Request:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/..."
}
Response:
{
"success": true,
"message": "Unsubscribed successfully"
}
GET /api/push/devices
Get all devices subscribed for the authenticated user.
Response:
{
"success": true,
"count": 3,
"data": {
"devices": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": "web-abc123",
"deviceName": "Chrome on macOS",
"browser": "Chrome",
"os": "macOS",
"deviceType": "desktop",
"isActive": true,
"lastUsedAt": "2025-10-22T10:30:00Z",
"createdAt": "2025-10-01T08:00:00Z"
}
]
}
}
POST /api/push/test/:userId (Admin Only)
Send a test notification to a specific user.
Request:
{
"title": "Test Notification",
"body": "This is a test notification",
"url": "/",
"deviceId": "web-abc123" // Optional: target specific device
}
Response:
{
"success": true,
"message": "Test notification sent successfully",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": "web-abc123"
}
}
Frontend API
navigator.serviceWorker.register()
Register service worker for push notifications.
// Usage
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service worker registered:', registration);
Notification.requestPermission()
Request notification permission from user.
// Usage
const permission = await Notification.requestPermission();
console.log('Permission:', permission); // "granted", "denied", or "default"
Returns: Promise<NotificationPermission>
registration.pushManager.subscribe()
Subscribe to push notifications with VAPID key.
// Usage
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
console.log('Subscription:', subscription.toJSON());
Returns: Promise<PushSubscription>
registration.showNotification()
Show a notification (from service worker).
// Usage
self.registration.showNotification('Title', {
body: 'Notification body',
icon: '/icon-192x192.png',
badge: '/icon-72x72.png',
data: { url: '/bookings/123' },
actions: [
{ action: 'view', title: 'View', icon: '/view-icon.png' },
{ action: 'dismiss', title: 'Dismiss', icon: '/dismiss-icon.png' }
]
});
Push Payload Schema
interface PushPayload {
// Required
title: string; // Max 65 characters
body: string; // Max 240 characters
// Optional display
icon?: string; // URL to icon (192x192 recommended)
badge?: string; // URL to badge (72x72 recommended)
image?: string; // URL to large image
tag?: string; // Notification grouping ID
// Behavior
requireInteraction?: boolean; // Keep until dismissed
silent?: boolean; // Suppress sound
vibrate?: number[]; // Vibration pattern [ms]
renotify?: boolean; // Re-alert on tag update
// Navigation
url?: string; // URL to open on click
// Custom data
data?: {
[key: string]: any; // Custom payload data
};
// Actions (max 2 on mobile, 4 on desktop)
actions?: Array<{
action: string; // Action identifier
title: string; // Button text
icon?: string; // Button icon
}>;
}
Browser Compatibility Matrix
Desktop Browsers
Feature | Chrome | Firefox | Safari | Edge | Opera |
---|---|---|---|---|---|
Service Workers | ✅ 40+ | ✅ 44+ | ✅ 11.1+ | ✅ 17+ | ✅ 27+ |
Push API | ✅ 42+ | ✅ 44+ | ✅ 16+ | ✅ 17+ | ✅ 29+ |
Notification API | ✅ 42+ | ✅ 44+ | ✅ 16+ | ✅ 17+ | ✅ 29+ |
Background Sync | ✅ 49+ | ❌ No | ❌ No | ✅ 79+ | ✅ 36+ |
Notification Actions | ✅ 48+ | ❌ No | ⚠️ Limited | ✅ 18+ | ✅ 35+ |
Mobile Browsers
Feature | Chrome (Android) | Firefox (Android) | Safari (iOS) | Samsung Internet |
---|---|---|---|---|
Service Workers | ✅ 40+ | ✅ 44+ | ✅ 11.3+ | ✅ 4.0+ |
Push API | ✅ 42+ | ✅ 44+ | ⚠️ 16.4+ | ✅ 4.0+ |
Notification API | ✅ 42+ | ✅ 44+ | ⚠️ Limited | ✅ 4.0+ |
PWA Required | ❌ No | ❌ No | ✅ Yes | ❌ No |
Important Notes
iOS Safari:
- Push notifications only work if app is added to home screen (PWA)
- Must open app from home screen icon (not browser)
- Requires iOS 16.4+ and Safari 16.4+
- No support for notification actions
- Limited customization
Detection Code:
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isStandalone = window.navigator.standalone;
const isPWA = window.matchMedia('(display-mode: standalone)').matches;
if (isIOS && !isStandalone && !isPWA) {
// Show "Add to Home Screen" instructions
showPWAInstallPrompt();
}
Frequently Asked Questions (FAQ)
General Questions
Q: Do I need FCM or other third-party services?
A: No! This is a completely self-hosted solution. The browser's push service (FCM for Chrome, Mozilla Push for Firefox) is used internally by the browser, but you don't need an FCM account or API key. VAPID keys are all you need.
Q: How much does it cost to run?
A: $0 for push notifications. You only pay for:
- Your server infrastructure (backend + database)
- Domain and SSL certificate
- Optional monitoring tools
No per-message costs, no quotas, no vendor lock-in.
Q: What happens if my server goes down?
A: Notifications sent during downtime are lost. The Web Push Protocol has a TTL (Time To Live) of 30 days by default, but browser push services don't queue messages indefinitely. Implement:
- High availability (multiple backend instances)
- Health checks and auto-restart
- Monitoring and alerting
Q: Can I send notifications to iOS users?
A: Yes, but with limitations:
- Only works if app is added to home screen (PWA)
- User must open app from home screen icon
- Requires iOS 16.4+ and Safari 16.4+
- No notification actions
- About 30% of mobile users (as of 2025)
Consider fallback strategies like email or SMS for iOS users.
Technical Questions
Q: How do VAPID keys work?
A: VAPID (Voluntary Application Server Identification) uses asymmetric cryptography:
- Private Key: Kept secret on your server, signs push messages
- Public Key: Shared with browsers during subscription
- Signature: Browser validates that push came from your server
Think of it like SSH keys or SSL certificates.
Q: Can I reuse VAPID keys across environments?
A: Not recommended. Use different keys for:
- Development
- Staging
- Production
This prevents cross-environment issues and improves security.
Q: How many devices can a user have?
A: By default, 5 devices per user/admin. This is configurable:
// Change device limit
if count >= 10 { // Increase to 10
return errors.New("device limit reached")
}
Each browser on each device counts as one subscription.
Q: What's the maximum notification size?
A: Approximately 4KB total payload. Recommendations:
- Title: Max 65 characters
- Body: Max 240 characters
- Icon: URL only (image not embedded)
- Keep
data
object small
Q: How fast are notifications delivered?
A: Typically < 1 second from your server to user's device:
- Your server → Browser push service: ~50ms
- Browser push service → User device: ~500ms
- Service worker processing: ~50ms
Network conditions and device state affect delivery time.
Q: Can I send rich media notifications?
A: Yes! You can include:
- ✅ Icons (192x192 recommended)
- ✅ Badges (72x72)
- ✅ Images (any size, but keep < 1MB)
- ✅ Vibration patterns
- ✅ Action buttons (up to 4)
- ❌ Videos (not supported)
- ❌ Interactive forms (not supported)
Security Questions
Q: Is it secure?
A: Yes, if implemented correctly:
- ✅ VAPID authentication prevents spoofing
- ✅ HTTPS required for service workers
- ✅ End-to-end encryption (browser handles)
- ✅ User consent required
- ✅ Subscriptions tied to origin (can't spoof)
Follow security best practices in Part 7.
Q: Can someone steal my VAPID private key?
A: If compromised:
- Attacker can send push notifications as your app
- Rotate keys immediately (see Part 7)
- Users will need to resubscribe
- Monitor for unusual notification patterns
Store private keys securely:
- Environment variables (not in code)
- Secrets manager (AWS Secrets Manager, HashiCorp Vault)
- Kubernetes secrets
Q: Can users unsubscribe?
A: Yes, users can:
- Click "Unsubscribe" in your app UI
- Revoke permissions in browser settings
- Clear browser data (removes subscription)
Always provide an easy unsubscribe option.
Scaling Questions
Q: How many notifications can I send per second?
A: Depends on your infrastructure:
Setup | Notifications/second |
---|---|
Single server | ~100-500 |
3 replicas | ~300-1500 |
10 replicas | ~1000-5000 |
With rate limiting | Depends on limits |
Bottlenecks:
- Database queries
- HTTP connections to push services
- Goroutine limits
Q: Do I need a queue system?
A: Optional, but recommended for:
- High volume (>1000 notifications/second)
- Guaranteed delivery with retries
- Priority queuing
- Rate limiting across multiple servers
Use Redis, RabbitMQ, or AWS SQS.
Q: How do I handle multiple backend replicas?
A:
- Database Connection Pooling: Prevents connection exhaustion
- Stateless Services: No in-memory state
- Worker Coordination: Use distributed locks (Redis) for background workers
- Load Balancing: Round-robin or least-connections
See Part 5 for horizontal scaling strategies.
Troubleshooting Guide
Symptom: Notifications not showing up
Possible Causes:
- Permission denied
// Check permission
console.log('Permission:', Notification.permission);
// If "denied", user must manually enable in browser settings
- Service worker not registered
// Check registration
navigator.serviceWorker.getRegistrations().then(regs => {
console.log('Registrations:', regs);
});
- Subscription expired
// Check subscription
navigator.serviceWorker.ready.then(async reg => {
const sub = await reg.pushManager.getSubscription();
console.log('Subscription:', sub);
});
- Backend error
# Check backend logs
kubectl logs -f deployment/user-service | grep "Push"
Symptom: High failure rate
Debugging Steps:
-- Check error messages
SELECT
error_message,
http_status_code,
COUNT(*) as count
FROM push_notification_logs
WHERE status = 'failed'
AND sent_at > NOW() - INTERVAL '1 hour'
GROUP BY error_message, http_status_code
ORDER BY count DESC;
Common Solutions:
Error | Solution |
---|---|
410 Gone | Subscription expired (auto-handled) |
403 Forbidden | Check VAPID keys |
400 Bad Request | Validate payload format |
429 Too Many Requests | Implement rate limiting |
Symptom: Memory leak
Debugging:
# Check memory usage
go tool pprof http://localhost:6060/debug/pprof/heap
# Check goroutines
go tool pprof http://localhost:6060/debug/pprof/goroutine
Common Causes:
- Goroutine leaks (not closing channels)
- Database connection leaks
- Unbounded caches
Symptom: Slow notification delivery
Debugging:
-- Check p95 latency
SELECT
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY (sent_at - created_at)) as p95_latency
FROM push_notification_logs
WHERE sent_at > NOW() - INTERVAL '1 hour';
Optimization:
- Add database indexes
- Increase connection pool size
- Use goroutine pooling
- Implement caching
Migration Guides
From FCM to Self-Hosted
Step 1: Generate VAPID Keys
npm install -g web-push
web-push generate-vapid-keys
Step 2: Update Backend
- Remove FCM SDK
- Install
webpush-go
- Implement PushService (see Part 2)
Step 3: Update Frontend
- Remove FCM
firebase-messaging-sw.js
- Implement
sw.js
(see Part 4) - Update subscription flow
Step 4: Migrate Existing Subscriptions
-- Map FCM tokens to new subscriptions
-- Users will need to resubscribe
-- Consider grace period with both systems running
From OneSignal to Self-Hosted
Key Differences:
Feature | OneSignal | Self-Hosted |
---|---|---|
Setup Time | 10 minutes | 2-4 hours |
Cost | Free tier, then $99+/mo | Infrastructure only |
Vendor Lock-in | Yes | No |
Customization | Limited | Complete |
Analytics | Built-in | DIY (Prometheus) |
Migration Steps: Same as FCM migration above.
Glossary of Terms
VAPID
Voluntary Application Server Identification. Authentication mechanism using public/private key pairs to verify push notification origin.
Service Worker
JavaScript file that runs in the background, separate from web pages. Enables push notifications, offline support, and background sync.
Push Subscription
Object containing endpoint URL and encryption keys that identifies a device for push delivery.
Endpoint
HTTPS URL provided by browser's push service (e.g., FCM, Mozilla Push) where notifications are sent.
p256dh
Public Key for Diffie-Hellman. Used to encrypt notification payloads.
auth
Authentication Secret. Used alongside p256dh for encryption.
TTL
Time To Live. How long push services keep notifications if device is offline (default: 30 days).
PWA
Progressive Web App. Web application that can be installed like a native app and supports offline functionality.
Goroutine
Lightweight thread in Go for concurrent execution.
GORM
Go Object-Relational Mapping. Database ORM library for Go.
JWT
JSON Web Token. Token-based authentication standard.
Additional Resources
Official Specifications
- Web Push Protocol: RFC 8030
- VAPID: RFC 8292
- Service Workers: W3C Specification
- Push API: MDN Web Docs
- Notification API: MDN Web Docs
Libraries & Tools
Backend (Go):
- webpush-go - Web Push Protocol implementation
- Gin - HTTP web framework
- GORM - ORM library
Frontend (JavaScript/TypeScript):
- web-push - Node.js Web Push library
- Workbox - Service worker toolkit
- next-pwa - PWA plugin for Next.js
Testing:
- Playwright - E2E testing
- k6 - Load testing
Monitoring:
- Prometheus - Metrics collection
- Grafana - Visualization
- Sentry - Error tracking
Community & Support
-
Stack Overflow: Tag
[web-push]
or[service-worker]
- GitHub Discussions: Create discussions on this repo
- MDN Web Docs: Comprehensive browser API documentation
- Can I Use: Browser compatibility checker (caniuse.com)
Learning Resources
- Service Worker Cookbook: serviceworke.rs
- PWA Guide: web.dev/progressive-web-apps
- Web Push Book: web-push-book.gauntface.com
Specification Summary
What You've Built
By following this 8-part specification, you've implemented:
✅ Complete self-hosted push notification system
✅ No third-party dependencies (FCM, OneSignal, etc.)
✅ Production-ready backend (Go with GORM)
✅ Production-ready frontend (Next.js with TypeScript)
✅ Service worker with background handling
✅ PWA manifest for installability
✅ Multi-device support (up to 5 per user)
✅ Advanced targeting (users, admins, roles, broadcast)
✅ Notification scheduling with background workers
✅ Rate limiting & throttling
✅ Batch processing for scalability
✅ Retry strategies with exponential backoff
✅ Docker & Kubernetes deployment configs
✅ Monitoring & alerting (Prometheus, Grafana)
✅ Health checks and error tracking
✅ Security best practices (VAPID, HTTPS, validation)
✅ Performance optimization (indexing, pooling, caching)
✅ Comprehensive testing (unit, integration, E2E, load)
Specification Parts Recap
Part | Title | Topics Covered |
---|---|---|
Part 1 | Architecture & Setup | System overview, VAPID, database schema, project structure |
Part 2 | Backend Implementation | GORM models, PushService, controllers, routes, middleware |
Part 3 | Frontend Implementation | API routes, hooks, components, TypeScript types |
Part 4 | Service Worker & PWA | SW implementation, PWA manifest, action handlers, offline |
Part 5 | Advanced & Deployment | Targeting, scheduling, rate limiting, Docker, Kubernetes |
Part 6 | Monitoring & Debugging | Metrics, logging, profiling, health checks, alerting |
Part 7 | Best Practices & Security | Security, VAPID management, validation, optimization, testing |
Part 8 | Reference & FAQ | API reference, browser compatibility, FAQ, troubleshooting |
Estimated Implementation Time
Component | Time Estimate |
---|---|
Setup & Architecture | 2-4 hours |
Backend Implementation | 8-12 hours |
Frontend Implementation | 6-10 hours |
Service Worker & PWA | 4-6 hours |
Testing & QA | 6-8 hours |
Deployment & Monitoring | 4-6 hours |
Documentation | 2-4 hours |
Total | 32-50 hours |
With AI assistance (agents), implementation time can be reduced by 50-70%.
Production Readiness Checklist
Before going live:
- [ ] VAPID keys generated and secured
- [ ] Database migrations applied
- [ ] All environment variables configured
- [ ] Service workers registered and tested
- [ ] PWA manifest configured
- [ ] SSL/HTTPS enabled
- [ ] Rate limiting configured
- [ ] Monitoring and alerting set up
- [ ] Error tracking integrated (Sentry)
- [ ] Health checks passing
- [ ] Load testing completed (target throughput achieved)
- [ ] Security audit passed
- [ ] Backup strategy in place
- [ ] Documentation complete
- [ ] Team trained
- [ ] Rollback plan prepared
Next Steps After Implementation
- Monitor metrics for first 24 hours
- Gather user feedback on notification experience
- Optimize based on real-world usage patterns
- A/B test notification content and timing
- Iterate on targeting strategies
- Scale infrastructure as needed
- Plan key rotation (6-12 months)
Final Notes
This Specification is Open Source
Feel free to:
- ✅ Use this specification for your projects
- ✅ Adapt and modify as needed
- ✅ Share with your team
- ✅ Contribute improvements
Contributing
If you find issues or have improvements:
- Open an issue on GitHub
- Submit a pull request
- Share your implementation stories
- Help others in discussions
License
MIT License - Free to use, modify, and distribute.
Author
Researched and Implemented with ❤️ by Bunty9
Congratulations! 🎉
You've completed the Self-Hosted Push Notifications Specification.
You now have the knowledge and code to build a complete, production-ready push notification system without any third-party dependencies.
Happy coding! 🚀
Top comments (0)