// Supabase Edge Function — fan-out push notifications via Web Push (VAPID). // // Trigger: server-side (e.g. database trigger on messages INSERT calling // pg_net.http_post → this function), or app-level after sendEncryptedMessage // resolves successfully. // // Required environment variables (set via supabase secrets): // VAPID_PUBLIC_KEY // VAPID_PRIVATE_KEY // VAPID_SUBJECT e.g. "mailto:admin@example.com" // PUSH_FANOUT_SHARED_SECRET shared header secret to authenticate caller // // Request body: // { conversationId: string, senderUserId: string, senderName: string, kind?: "message" | "call" } // // The function: // 1. Looks up conversation_members minus sender. // 2. Joins to push_tokens via devices. // 3. Sends a Web Push to each token using web-push npm via npm: specifier. // // Note: this is a skeleton — wire web-push library and verify against the // chosen Deno deploy runtime. // deno-lint-ignore-file no-explicit-any import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import webPush from 'npm:web-push@3.6.7'; const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? ''; const SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''; const VAPID_PUBLIC_KEY = Deno.env.get('VAPID_PUBLIC_KEY') ?? ''; const VAPID_PRIVATE_KEY = Deno.env.get('VAPID_PRIVATE_KEY') ?? ''; const VAPID_SUBJECT = Deno.env.get('VAPID_SUBJECT') ?? 'mailto:admin@example.com'; const SHARED_SECRET = Deno.env.get('PUSH_FANOUT_SHARED_SECRET') ?? ''; if (VAPID_PUBLIC_KEY && VAPID_PRIVATE_KEY) { webPush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY); } interface Payload { conversationId: string; senderUserId: string; senderName: string; kind?: 'message' | 'call'; } Deno.serve(async (req) => { if (req.method !== 'POST') return new Response('method', { status: 405 }); // Shared-secret gate. Cheap, replaces user JWT here because the trigger has // no authenticated session. const auth = req.headers.get('x-shared-secret'); if (!SHARED_SECRET || auth !== SHARED_SECRET) { return new Response('unauthorized', { status: 401 }); } let body: Payload; try { body = await req.json(); } catch { return new Response('bad json', { status: 400 }); } const client = createClient(SUPABASE_URL, SERVICE_ROLE_KEY); const { data: members, error: mErr } = await client .from('conversation_members') .select('user_id') .eq('conversation_id', body.conversationId); if (mErr) return new Response('members lookup failed', { status: 500 }); const recipientIds = (members ?? []) .map((m: any) => m.user_id as string) .filter((id: string) => id !== body.senderUserId); if (recipientIds.length === 0) return new Response('no recipients', { status: 200 }); const { data: tokens, error: tErr } = await client .from('push_tokens') .select('token, devices!inner(user_id)') .in('devices.user_id', recipientIds); if (tErr) return new Response('tokens lookup failed', { status: 500 }); const payload = JSON.stringify({ title: body.senderName, body: body.kind === 'call' ? 'Eingehender Anruf' : 'Neue Nachricht', conversationId: body.conversationId, kind: body.kind ?? 'message', }); const results = await Promise.allSettled( (tokens ?? []).map(async (row: any) => { try { const sub = JSON.parse(row.token); await webPush.sendNotification(sub, payload); } catch (err) { console.warn('push send failed', err); } }), ); return new Response(JSON.stringify({ sent: results.length }), { headers: { 'content-type': 'application/json' }, }); });