import 'server-only' import webpush from 'web-push' import { prisma } from '@/lib/prisma' export type PushPayload = { title: string body: string url: string tag?: string } const vapidReady = !!process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY && !!process.env.VAPID_PRIVATE_KEY && !!process.env.VAPID_SUBJECT if (vapidReady) { webpush.setVapidDetails( process.env.VAPID_SUBJECT!, process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY!, ) } export const enabled = vapidReady export async function sendPushToUser(userId: string, payload: PushPayload): Promise { if (!enabled) { console.warn('[push-server] VAPID not configured — skipping push for user', userId) return } const subs = await prisma.pushSubscription.findMany({ where: { user_id: userId } }) await Promise.allSettled(subs.map((sub) => sendOne(sub, payload))) } async function sendOne( sub: { id: string; endpoint: string; p256dh: string; auth: string }, payload: PushPayload, ): Promise { try { await webpush.sendNotification( { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } }, JSON.stringify(payload), ) await prisma.pushSubscription.update({ where: { id: sub.id }, data: { last_used_at: new Date() }, }) } catch (err: unknown) { const status = (err as { statusCode?: number }).statusCode if (status === 404 || status === 410) { try { await prisma.pushSubscription.delete({ where: { id: sub.id } }) } catch { // already deleted by a concurrent request — ignore } } else { console.error('[push-server] sendNotification error for endpoint', sub.endpoint, err) } } }