Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosGoogle Cloud Tech: Gemini is coming to your city(24.09.2026 um 15:00 Uhr)
AI & KI NachrichtenGoogle’s latest moonshot to put machine learning in space(24.09.2026 um 15:12 Uhr)
Windows Tipps & SecurityPoll: What's your favorite Surface of 2026?(24.09.2026 um 14:58 Uhr)
Sichere ProgrammierungStreaming Materialized Views for Live Read Models (2026)(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungA Day Is Not 86400 Seconds: The DST Bug in Your Date Math(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungSetting up Traefik: reverse proxy with automatic HTTPS(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungA 200 OK response does not prove a secret leak(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungHow hot do you like it?(24.09.2026 um 15:05 Uhr)
YouTube Security VideosGoogle Cloud Tech: Gemini is coming to your city(24.09.2026 um 15:00 Uhr)
AI & KI NachrichtenGoogle’s latest moonshot to put machine learning in space(24.09.2026 um 15:12 Uhr)
Windows Tipps & SecurityPoll: What's your favorite Surface of 2026?(24.09.2026 um 14:58 Uhr)
Sichere ProgrammierungStreaming Materialized Views for Live Read Models (2026)(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungA Day Is Not 86400 Seconds: The DST Bug in Your Date Math(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungSetting up Traefik: reverse proxy with automatic HTTPS(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungA 200 OK response does not prove a secret leak(24.09.2026 um 15:02 Uhr)
Sichere ProgrammierungHow hot do you like it?(24.09.2026 um 15:05 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

# Self-Hosted Push Notifications Part-3

Self-Hosted Push Notifications Specification Part 3: Frontend Implementation (Next.js) Version: 1.0 Last Updated: October 2025 Prerequisites: Part 2: Backend Implementation Author: Bunty9 License: MIT (Free to use and…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Self-Hosted Push Notifications Specification






Part 3: Frontend Implementation (Next.js)



Version: 1.0

Last Updated: October 2025

Prerequisites: Part 2: Backend Implementation

Author: Bunty9

License: MIT (Free to use and adapt)







Table of Contents




  1. Dependencies

  2. TypeScript Type Definitions

  3. API Routes (SSR)

  4. Custom Hook

  5. PushNotificationManager Component

  6. Utility Functions

  7. Integration in _app.tsx

  8. Usage Examples

  9. Troubleshooting







Dependencies





Install Required Packages





# Navigate to frontend directory
cd frontend

# Create Next.js app if you haven't already
npx create-next-app@latest . --typescript

# No additional packages needed!
# Push API is built into modern browsers







package.json (Relevant Dependencies)





{
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}









TypeScript Type Definitions





types/push.ts





// ==================== Request/Response Types ====================

export interface PushSubscriptionKeys {
p256dh: string;
auth: string;
}

export interface SubscribePushRequest {
endpoint: string;
keys: PushSubscriptionKeys;
userAgent: string;
}

export interface SubscribePushResponse {
success: boolean;
message: string;
subscriptionId: string;
deviceId: string;
deviceName: string;
}

export interface UnsubscribePushRequest {
endpoint: string;
}

export interface DeviceInfo {
id: string;
deviceId: string;
deviceName: string;
browser: string;
os: string;
deviceType: 'mobile' | 'desktop' | 'tablet';
isActive: boolean;
lastUsedAt?: string;
createdAt: string;
}

export interface GetDevicesResponse {
success: boolean;
count: number;
devices: DeviceInfo[];
}

// ==================== Notification Payload ====================

export interface NotificationAction {
action: string;
title: string;
icon?: string;
}

export interface PushPayload {
title: string;
body: string;
icon?: string;
badge?: string;
image?: string;
tag?: string;
url?: string;
data?: Record<string, any>;
requireInteraction?: boolean;
silent?: boolean;
vibrate?: number[];
actions?: NotificationAction[];
}

// ==================== Browser API Types ====================

export interface PushSubscriptionOptions {
userVisibleOnly: boolean;
applicationServerKey: Uint8Array;
}

// ==================== Component State ====================

export interface PushNotificationState {
isSupported: boolean;
permission: NotificationPermission;
isSubscribed: boolean;
isLoading: boolean;
error: string | null;
}









API Routes (SSR)



These routes act as secure proxies to your Go backend, forwarding cookies for authentication.





pages/api/push/subscribe.ts





import type { NextApiRequest, NextApiResponse } from 'next';

const USER_SERVICE_URL = process.env.BACKEND_API_URL || 'http://localhost:8080';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const { endpoint, keys, userAgent } = req.body;

// Validate request
if (!endpoint || !keys || !keys.p256dh || !keys.auth) {
return res.status(400).json({ error: 'Missing required fields' });
}

// Forward to backend with cookies
const cookieHeader = req.headers.cookie || '';

const response = await fetch(`${USER_SERVICE_URL}/api/push/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': cookieHeader, // Critical: Forward entire cookie header
},
body: JSON.stringify({
endpoint,
keys,
userAgent: userAgent || req.headers['user-agent'] || '',
}),
});

const data = await response.json();

if (!response.ok) {
return res.status(response.status).json(data);
}

return res.status(200).json(data);
} catch (error) {
console.error('Subscribe error:', error);
return res.status(500).json({
error: 'Failed to subscribe to push notifications'
});
}
}







pages/api/push/unsubscribe.ts





import type { NextApiRequest, NextApiResponse } from 'next';

const USER_SERVICE_URL = process.env.BACKEND_API_URL || 'http://localhost:8080';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const { endpoint } = req.body;

if (!endpoint) {
return res.status(400).json({ error: 'Endpoint is required' });
}

const cookieHeader = req.headers.cookie || '';

const response = await fetch(`${USER_SERVICE_URL}/api/push/unsubscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': cookieHeader,
},
body: JSON.stringify({ endpoint }),
});

const data = await response.json();

if (!response.ok) {
return res.status(response.status).json(data);
}

return res.status(200).json(data);
} catch (error) {
console.error('Unsubscribe error:', error);
return res.status(500).json({
error: 'Failed to unsubscribe from push notifications'
});
}
}







pages/api/push/devices.ts





import type { NextApiRequest, NextApiResponse } from 'next';

const USER_SERVICE_URL = process.env.BACKEND_API_URL || 'http://localhost:8080';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const cookieHeader = req.headers.cookie || '';

const response = await fetch(`${USER_SERVICE_URL}/api/push/devices`, {
method: 'GET',
headers: {
'Cookie': cookieHeader,
},
});

const data = await response.json();

if (!response.ok) {
return res.status(response.status).json(data);
}

return res.status(200).json(data);
} catch (error) {
console.error('Get devices error:', error);
return res.status(500).json({
error: 'Failed to fetch devices'
});
}
}









Custom Hook





hooks/usePushNotifications.ts



A reusable React hook that encapsulates all push notification logic.




import { useState, useEffect, useCallback } from 'react';
import type { PushNotificationState, DeviceInfo } from '@/types/push';

const VAPID_PUBLIC_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || '';

export const usePushNotifications = () => {
const [state, setState] = useState<PushNotificationState>({
isSupported: false,
permission: 'default',
isSubscribed: false,
isLoading: false,
error: null,
});

// Check if push notifications are supported
useEffect(() => {
if (typeof window === 'undefined') return;

const isSupported =
'serviceWorker' in navigator &&
'PushManager' in window &&
'Notification' in window;

setState((prev) => ({
...prev,
isSupported,
permission: isSupported ? Notification.permission : 'denied',
}));
}, []);

// Check if already subscribed
useEffect(() => {
if (!state.isSupported) return;

checkSubscription();
}, [state.isSupported]);

// Check if user is already subscribed
const checkSubscription = async () => {
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();

setState((prev) => ({
...prev,
isSubscribed: subscription !== null,
permission: Notification.permission,
}));
} catch (error) {
console.error('Failed to check subscription:', error);
}
};

// Request notification permission
const requestPermission = async (): Promise<NotificationPermission> => {
if (!state.isSupported) {
throw new Error('Push notifications are not supported');
}

const permission = await Notification.requestPermission();
setState((prev) => ({ ...prev, permission }));
return permission;
};

// Subscribe to push notifications
const subscribe = useCallback(async () => {
setState((prev) => ({ ...prev, isLoading: true, error: null }));

try {
// Check support
if (!state.isSupported) {
throw new Error('Push notifications are not supported in this browser');
}

// Request permission if not granted
if (Notification.permission === 'default') {
const permission = await requestPermission();
if (permission !== 'granted') {
throw new Error('Notification permission denied');
}
} else if (Notification.permission !== 'granted') {
throw new Error('Notification permission denied');
}

// Wait for service worker to be ready
const registration = await navigator.serviceWorker.ready;

// Subscribe to push manager
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});

// Convert subscription to JSON
const subscriptionJSON = subscription.toJSON();

// Send subscription to backend
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: subscriptionJSON.endpoint,
keys: {
p256dh: subscriptionJSON.keys?.p256dh || '',
auth: subscriptionJSON.keys?.auth || '',
},
userAgent: navigator.userAgent,
}),
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to subscribe');
}

const data = await response.json();

setState((prev) => ({
...prev,
isSubscribed: true,
isLoading: false,
error: null,
}));

return data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to subscribe';
setState((prev) => ({
...prev,
isLoading: false,
error: errorMessage,
}));
throw error;
}
}, [state.isSupported]);

// Unsubscribe from push notifications
const unsubscribe = useCallback(async () => {
setState((prev) => ({ ...prev, isLoading: true, error: null }));

try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();

if (!subscription) {
throw new Error('No subscription found');
}

// Unsubscribe from push manager
await subscription.unsubscribe();

// Notify backend
await fetch('/api/push/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: subscription.endpoint,
}),
});

setState((prev) => ({
...prev,
isSubscribed: false,
isLoading: false,
error: null,
}));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to unsubscribe';
setState((prev) => ({
...prev,
isLoading: false,
error: errorMessage,
}));
throw error;
}
}, []);

// Get user's devices
const getDevices = useCallback(async (): Promise<DeviceInfo[]> => {
try {
const response = await fetch('/api/push/devices');

if (!response.ok) {
throw new Error('Failed to fetch devices');
}

const data = await response.json();
return data.data.devices || [];
} catch (error) {
console.error('Failed to get devices:', error);
return [];
}
}, []);

return {
...state,
subscribe,
unsubscribe,
requestPermission,
checkSubscription,
getDevices,
};
};

// ==================== Utility Functions ====================

/**
* Converts a base64 URL-safe string to Uint8Array (required for VAPID key)
*/

function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');

const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);

for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

/**
* Converts ArrayBuffer to base64 string
*/

function arrayBufferToBase64(buffer: ArrayBuffer | null): string {
if (!buffer) return '';
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}












PushNotificationManager Component






components/PushNotificationManager.tsx



A self-contained UI component that handles subscription management.




'use client';

import { useEffect, useState } from 'react';
import { usePushNotifications } from '@/hooks/usePushNotifications';

interface PushNotificationManagerProps {
className?: string;
showDismiss?: boolean; // Allow users to dismiss the prompt
autoHide?: boolean; // Auto-hide after successful subscription
autoHideDelay?: number; // Delay before auto-hide (ms)
}

export const PushNotificationManager = ({
className = '',
showDismiss = true,
autoHide = true,
autoHideDelay = 3000,
}: PushNotificationManagerProps) => {
const {
isSupported,
permission,
isSubscribed,
isLoading,
error,
subscribe,
unsubscribe,
} = usePushNotifications();

const [isVisible, setIsVisible] = useState(true);
const [isClient, setIsClient] = useState(false);
const [isDismissed, setIsDismissed] = useState(false);

// Check if dismissed in localStorage
useEffect(() => {
setIsClient(true);
const dismissed = localStorage.getItem('push-notification-dismissed');
if (dismissed === 'true') {
setIsDismissed(true);
setIsVisible(false);
}
}, []);

// Auto-hide after successful subscription
useEffect(() => {
if (isSubscribed && autoHide) {
const timer = setTimeout(() => {
setIsVisible(false);
}, autoHideDelay);

return () => clearTimeout(timer);
}
}, [isSubscribed, autoHide, autoHideDelay]);

const handleSubscribe = async () => {
try {
await subscribe();
} catch (error) {
console.error('Failed to subscribe:', error);
}
};

const handleUnsubscribe = async () => {
try {
await unsubscribe();
} catch (error) {
console.error('Failed to unsubscribe:', error);
}
};

const handleDismiss = () => {
setIsVisible(false);
setIsDismissed(true);
localStorage.setItem('push-notification-dismissed', 'true');
};

// Don't render on server
if (!isClient) {
return null;
}

// Don't show if dismissed or not visible
if (!isVisible || isDismissed) {
return null;
}

// Don't show if not supported
if (!isSupported) {
return null;
}

// Already subscribed - show success message
if (isSubscribed) {
return (
<div className={`notification-banner success ${className}`}>
<div className="notification-content">
<div className="notification-icon"></div>
<div className="notification-text">
<p className="notification-title">Notifications Enabled</p>
<p className="notification-body">
You'll receive important updates and notifications
</p>
</div>
</div>
<div className="notification-actions">
<button
onClick={handleUnsubscribe}
className="btn-secondary"
disabled={isLoading}
>
Unsubscribe
</button>
{showDismiss && (
<button onClick={handleDismiss} className="btn-close">
×
</button>
)}
</div>
</div>
);
}

// Permission denied
if (permission ===
'denied') {
return (
<div className={`notification-banner warning ${className}`}>
<div className="notification-content">
<div className="notification-icon">⚠️</div>
<div className="notification-text">
<p className="notification-title">Notifications Blocked</p>
<p className="notification-body">
You
've blocked notifications. Enable them in your browser settings.
</p>
</div>
</div>
{showDismiss && (
<button onClick={handleDismiss} className="btn-close">
×
</button>
)}
</div>
);
}

// Prompt to enable notifications
return (
<div className={`notification-banner info ${className}`}>
<div className="notification-content">
<div className="notification-icon">🔔</div>
<div className="notification-text">
<p className="notification-title">Stay Updated</p>
<p className="notification-body">
Enable notifications to receive important updates
</p>
</div>
</div>
<div className="notification-actions">
<button
onClick={handleSubscribe}
className="btn-primary"
disabled={isLoading}
>
{isLoading ? 'Enabling...' : 'Enable Notifications'}
</button>
{showDismiss && (
<button onClick={handleDismiss} className="btn-close">
×
</button>
)}
</div>
{error && (
<div className="notification-error">
<p>{error}</p>
</div>
)}
</div>
);
};

// ==================== Styles ====================

// Add to your global CSS or styled-components

/*
.notification-banner {
position: fixed;
bottom: 20px;
right: 20px;
max-width: 400px;
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: white;
z-index: 1000;
animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

.notification-banner.success {
border-left: 4px solid #10b981;
}

.notification-banner.warning {
border-left: 4px solid #f59e0b;
}

.notification-banner.info {
border-left: 4px solid #3b82f6;
}

.notification-content {
display: flex;
align-items: start;
gap: 12px;
margin-bottom: 12px;
}

.notification-icon {
font-size: 24px;
flex-shrink: 0;
}

.notification-text {
flex: 1;
}

.notification-title {
font-weight: 600;
margin-bottom: 4px;
}

.notification-body {
font-size: 14px;
color: #6b7280;
}

.notification-actions {
display: flex;
gap: 8px;
align-items: center;
justify-content: flex-end;
}

.btn-primary {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}

.btn-primary:hover {
background: #2563eb;
}

.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.btn-secondary {
padding: 8px 16px;
background: #f3f4f6;
color: #374151;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}

.btn-secondary:hover {
background: #e5e7eb;
}

.btn-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #9ca3af;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}

.btn-close:hover {
color: #6b7280;
}

.notification-error {
margin-top: 8px;
padding: 8px;
background: #fee2e2;
border-radius: 4px;
font-size: 14px;
color: #991b1b;
}

@media (max-width: 640px) {
.notification-banner {
bottom: 0;
right: 0;
left: 0;
max-width: none;
border-radius: 0;
}
}
*/













Utility Functions






utils/push-utils.ts






/**
* Registers the service worker
*/

export async function registerServiceWorker(): Promise<ServiceWorkerRegistration | null> {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration);
return registration;
} catch (error) {
console.error('Service Worker registration failed:', error);
return null;
}
}
return null;
}

/**
* Checks if push notifications are supported
*/

export function isPushNotificationSupported(): boolean {
return (
typeof window !== 'undefined' &&
'serviceWorker' in navigator &&
'PushManager' in window &&
'Notification' in window
);
}

/**
* Gets the current notification permission
*/

export function getNotificationPermission(): NotificationPermission {
if (typeof window === 'undefined' || !('Notification' in window)) {
return 'denied';
}
return Notification.permission;
}

/**
* Checks if user is subscribed to push notifications
*/

export async function isSubscribed(): Promise<boolean> {
if (!isPushNotificationSupported()) {
return false;
}

try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
return subscription !== null;
} catch (error) {
console.error('Failed to check subscription status:', error);
return false;
}
}

/**
* Shows a local notification (for testing)
*/

export async function showLocalNotification(
title: string,
body: string,
options?: NotificationOptions
): Promise<void> {
if (!isPushNotificationSupported()) {
throw new Error('Notifications not supported');
}

if (Notification.permission !== 'granted') {
throw new Error('Notification permission not granted');
}

const registration = await navigator.serviceWorker.ready;
await registration.showNotification(title, {
body,
...options,
});
}












Integration in _app.tsx






pages/_app.tsx






import { useEffect } from 'react';
import type { AppProps } from 'next/app';
import { PushNotificationManager } from '@/components/PushNotificationManager';
import { registerServiceWorker } from '@/utils/push-utils';
import '@/styles/globals.css';

export default function App({ Component, pageProps }: AppProps) {
// Register service worker on app mount
useEffect(() => {
registerServiceWorker();
}, []);

return (
<>
<Component {...pageProps} />

{/* Add PushNotificationManager globally */}
<PushNotificationManager
showDismiss={true}
autoHide={true}
autoHideDelay={3000}
/>
</>
);
}









pages/_document.tsx



CRITICAL: Link PWA manifest in HTML head.




import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
return (
<Html lang="en">
<Head>
{/* PWA Manifest - REQUIRED for notifications */}
<link rel="manifest" href="/manifest.json" />

{/* PWA Icons */}
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icon-192x192.png" />

{/* Theme Color */}
<meta name="theme-color" content="#3b82f6" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}












Usage Examples






Example 1: Programmatic Notification






// In any component
import { usePushNotifications } from '@/hooks/usePushNotifications';

export function MyComponent() {
const { subscribe, isSubscribed, isLoading } = usePushNotifications();

const handleEnableNotifications = async () => {
try {
await subscribe();
alert('Notifications enabled!');
} catch (error) {
alert('Failed to enable notifications');
}
};

return (
<button onClick={handleEnableNotifications} disabled={isLoading || isSubscribed}>
{isSubscribed ? 'Notifications Enabled' : 'Enable Notifications'}
</button>
);
}









Example 2: Device Management UI






import { useEffect, useState } from 'react';
import { usePushNotifications } from '@/hooks/usePushNotifications';
import type { DeviceInfo } from '@/types/push';

export function DeviceList() {
const { getDevices } = usePushNotifications();
const [devices, setDevices] = useState<DeviceInfo[]>([]);

useEffect(() => {
fetchDevices();
}, []);

const fetchDevices = async () => {
const deviceList = await getDevices();
setDevices(deviceList);
};

return (
<div className="device-list">
<h3>Your Devices ({devices.length}/5)</h3>
{devices.map((device) => (
<div key={device.id} className="device-card">
<div className="device-info">
<h4>{device.deviceName}</h4>
<p>{device.browser} {device.os}</p>
<p className="device-date">
Last used: {device.lastUsedAt ? new Date(device.lastUsedAt).toLocaleDateString() : 'Never'}
</p>
</div>
<div className="device-status">
{device.isActive ? (
<span className="status-active">Active</span>
) : (
<span className="status-inactive">Inactive</span>
)}
</div>
</div>
))}
</div>
);
}









Example 3: Conditional Rendering






import { usePushNotifications } from '@/hooks/usePushNotifications';

export function NotificationBell() {
const { isSupported, isSubscribed, permission } = usePushNotifications();

// Don't show bell if not supported
if (!isSupported) {
return null;
}

return (
<button className="notification-bell">
🔔
{isSubscribed ? (
<span className="badge"></span>
) : permission === 'denied' ? (
<span className="badge"></span>
) : (
<span className="badge">!</span>
)}
</button>
);
}









Example 4: Backend-Triggered Notification






// Backend logic (Go) - when booking is confirmed
func (bs *BookingService) ConfirmBooking(bookingID uuid.UUID) error {
// ... confirm booking logic ...

// Send push notification
payload := types.PushPayload{
Title: "Booking Confirmed!",
Body: fmt.Sprintf("Your booking at %s is confirmed", spaceName),
Icon: "/icon-192x192.png",
Tag: "booking_confirmed",
URL: fmt.Sprintf("/bookings/%s", bookingID),
Data: map[string]interface{}{
"bookingId": bookingID.String(),
"type": "booking_confirmed",
},
}

// Non-blocking send
go pushService.SendToUser(userID, payload)

return nil
}












Troubleshooting






Issue 1: "Service worker registration failed"



Cause: Service worker file not found or HTTPS not enabled.



Solution:




# Check file exists
ls public/sw.js

# Development: Next.js serves /public at root
# Access at: http://localhost:3000/sw.js

# Production: Ensure HTTPS is enabled









Issue 2: "Notification permission denied"



Cause: User clicked "Block" or "Deny".



Solution:




  1. User must manually reset permissions in browser settings

  2. Chrome: Settings → Privacy → Site Settings → Notifications

  3. Firefox: Preferences → Privacy → Permissions → Notifications






Issue 3: "VAPID public key not configured"



Cause: Missing environment variable.



Solution:




# Create .env.local
echo "NEXT_PUBLIC_VAPID_PUBLIC_KEY=YOUR_PUBLIC_KEY_HERE" > .env.local

# Restart dev server
npm run dev









Issue 4: "Failed to subscribe: DOMException"



Cause: VAPID key mismatch or invalid format.



Solution:




// Verify key in hook
console.log('VAPID Key:', process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY);

// Ensure it's the same as backend
// Should be base64 URL-safe string, ~88 characters









Issue 5: "Notifications don't work on iOS"



Cause: iOS Safari requires PWA installation.



Solution:




  1. User must "Add to Home Screen"

  2. Open app from home screen icon (not browser)

  3. Then notifications will work



Detection Code:




const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isStandalone = (window.navigator as any).standalone;

if (isIOS && !isStandalone) {
// Show "Add to Home Screen" instruction
}









Issue 6: "Subscription works but no notifications received"



Cause: Service worker not handling push events.



Solution:




  1. Check service worker is registered: Chrome DevTools → Application → Service Workers

  2. Verify push event listener exists in sw.js

  3. Test with backend /api/push/test/:userId endpoint









Next Steps



Frontend implementation complete!



Now proceed to:



➡️ Part 4: Service Worker & PWA Configuration



Part 4 will cover:




  1. Complete service worker implementation

  2. Push event handling

  3. Notification display customization

  4. Click handlers and actions

  5. PWA manifest configuration

  6. Offline support

  7. Background sync (advanced)









Summary



You now have a complete, production-ready frontend for push notifications with:



✅ Type-safe API routes with cookie forwarding

✅ Reusable usePushNotifications hook

✅ PushNotificationManager component with UI

✅ Multi-device support

✅ Automatic permission handling

✅ Error boundaries and fallbacks

✅ localStorage persistence for dismissed prompts

✅ Comprehensive utility functions



The frontend seamlessly integrates with your Go backend through SSR API routes.

CTI Threat Relationship Graph2 Knoten / 1 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
SOC Incident Playbook: Vulnerability Remediation & Verification
title: Detect Exploitation - # Self-Hosted Push Notifications Part-3
id: 16f035e5-1364-4f0e-90de-fa739e01f780
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-24
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-24"
        description = "YARA Signature for "
    strings:
        $str = "# Self-Hosted Push Notificatio" ascii wide
    condition:
        any of them
}
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich # Self-Hosted Push Notifications Part-3.... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten # Self-Hosted Push Notifications Part-3

Thematisch verwandte Begriffe: SelfHosted, Push, Notifications, Part3 · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-97152 | Nanomsg versions 0.5-beta through 1.x before 1.2.3 has a remotely exploi…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick