Skip to main content
RapidDev - Software Development Agency

How to Build Notification system with V0

Build a multi-channel notification hub with V0 using Next.js, Supabase Realtime, Resend, web-push, and shadcn/ui. Delivers in-app, email, and push notifications with user preference controls, template rendering, and delivery tracking — all dispatched via Promise.allSettled for graceful partial failures. Takes about 2-4 hours.

What you'll build

  • Multi-channel dispatch engine sending to in-app, email, and push based on user preferences
  • Notification bell Popover with unread count Badge and ScrollArea list using Supabase Realtime
  • User preference settings with Switch toggles per notification category and channel
  • Template engine rendering notification titles and bodies from JSONB templates with variable interpolation
  • Push notification support with VAPID keys, service worker registration, and web-push library
  • Delivery tracking per channel with Promise.allSettled for handling partial failures
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced9 min read2-4 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a multi-channel notification hub with V0 using Next.js, Supabase Realtime, Resend, web-push, and shadcn/ui. Delivers in-app, email, and push notifications with user preference controls, template rendering, and delivery tracking — all dispatched via Promise.allSettled for graceful partial failures. Takes about 2-4 hours.

What you're building

SaaS products need notifications across multiple channels — in-app for active users, email for important updates, and push for time-sensitive alerts. Users must control which notifications they receive and how.

V0 generates the notification bell UI, preferences page, and dispatch API from prompts. Supabase Realtime powers instant in-app delivery, Resend handles email, and the web-push library sends browser push notifications.

The architecture uses a centralized send API that renders templates, checks user preferences, and dispatches to all enabled channels using Promise.allSettled for graceful handling of partial failures. Supabase Realtime shows new notifications instantly in the bell dropdown.

Final result

A notification system with multi-channel delivery (in-app, email, push), user preference controls, template rendering, and real-time in-app updates.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Supabase RealtimeRealtime
ResendEmail Delivery

Prerequisites

  • A V0 account (Premium or higher — this is a complex build)
  • A Supabase project with Realtime enabled (connect via V0's Connect panel)
  • A Resend account for email delivery (free tier: 100 emails/day)
  • VAPID keys for web push notifications (generate with web-push library)

Build steps

1

Set up the database schema for notifications and preferences

Create the Supabase schema for notification templates, notifications, user preferences, and push subscriptions.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a notification system. Create a Supabase schema:
3// 1. notification_templates: id (uuid PK), slug (text UNIQUE), title_template (text), body_template (text), channels (text[] DEFAULT '{in_app}'), category (text)
4// 2. notifications: id (uuid PK), user_id (uuid FK to auth.users), template_id (uuid FK to notification_templates), title (text), body (text), data (jsonb), channels_sent (text[]), is_read (boolean DEFAULT false), read_at (timestamptz), created_at (timestamptz)
5// 3. notification_preferences: user_id (uuid FK to auth.users), category (text), in_app (boolean DEFAULT true), email (boolean DEFAULT true), push (boolean DEFAULT false), PRIMARY KEY (user_id, category)
6// 4. push_subscriptions: id (uuid PK), user_id (uuid FK to auth.users), endpoint (text), keys (jsonb), created_at (timestamptz)
7// Enable Realtime on the notifications table. Seed templates for common events.
8// Add RLS: users see only their own notifications and preferences.

Pro tip: Enable Realtime on the notifications table in Supabase Dashboard > Database > Replication for instant in-app delivery.

Expected result: All tables are created with Realtime enabled on notifications. Templates are seeded for common notification events.

2

Build the multi-channel dispatch API

Create the send endpoint that renders templates, checks user preferences, and dispatches to all enabled channels. Uses Promise.allSettled to handle partial failures gracefully.

app/api/notifications/send/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3import { Resend } from 'resend'
4import webpush from 'web-push'
5
6const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)
7const resend = new Resend(process.env.RESEND_API_KEY)
8
9webpush.setVapidDetails('mailto:admin@yourdomain.com', process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY!)
10
11function renderTemplate(template: string, data: Record<string, string>): string {
12 return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] || '')
13}
14
15export async function POST(req: NextRequest) {
16 const { user_id, template_slug, data } = await req.json()
17
18 const { data: template } = await supabase.from('notification_templates').select('*').eq('slug', template_slug).single()
19 if (!template) return NextResponse.json({ error: 'Template not found' }, { status: 404 })
20
21 const title = renderTemplate(template.title_template, data)
22 const body = renderTemplate(template.body_template, data)
23
24 const { data: prefs } = await supabase.from('notification_preferences').select('*').eq('user_id', user_id).eq('category', template.category).single()
25 const userPrefs = prefs || { in_app: true, email: true, push: false }
26
27 const channels: string[] = []
28 const dispatches: Promise<any>[] = []
29
30 if (userPrefs.in_app) {
31 channels.push('in_app')
32 dispatches.push(supabase.from('notifications').insert({ user_id, template_id: template.id, title, body, data, channels_sent: channels }))
33 }
34
35 if (userPrefs.email) {
36 channels.push('email')
37 const { data: user } = await supabase.auth.admin.getUserById(user_id)
38 if (user?.user?.email) {
39 dispatches.push(resend.emails.send({ from: 'notifications@yourdomain.com', to: user.user.email, subject: title, html: `<p>${body}</p>` }))
40 }
41 }
42
43 if (userPrefs.push) {
44 channels.push('push')
45 const { data: subs } = await supabase.from('push_subscriptions').select('*').eq('user_id', user_id)
46 for (const sub of subs || []) {
47 dispatches.push(webpush.sendNotification({ endpoint: sub.endpoint, keys: sub.keys }, JSON.stringify({ title, body })))
48 }
49 }
50
51 const results = await Promise.allSettled(dispatches)
52 return NextResponse.json({ channels, results: results.map(r => r.status) })
53}

Expected result: The API dispatches to all enabled channels based on user preferences. Partial failures do not block other channels.

3

Build the notification bell with real-time updates

Create the notification bell component that shows unread count, streams new notifications via Supabase Realtime, and displays them in a Popover dropdown.

components/notification-bell.tsx
1'use client'
2
3import { useEffect, useState } from 'react'
4import { createBrowserClient } from '@supabase/ssr'
5import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
6import { Button } from '@/components/ui/button'
7import { Badge } from '@/components/ui/badge'
8import { ScrollArea } from '@/components/ui/scroll-area'
9import { Bell } from 'lucide-react'
10
11interface Notification { id: string; title: string; body: string; is_read: boolean; created_at: string }
12
13export function NotificationBell({ userId, initial }: { userId: string; initial: Notification[] }) {
14 const [notifications, setNotifications] = useState(initial)
15 const unread = notifications.filter(n => !n.is_read).length
16 const supabase = createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
17
18 useEffect(() => {
19 const channel = supabase.channel(`notifications:${userId}`)
20 .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications', filter: `user_id=eq.${userId}` }, (payload) => {
21 setNotifications(prev => [payload.new as Notification, ...prev])
22 }).subscribe()
23 return () => { supabase.removeChannel(channel) }
24 }, [userId, supabase])
25
26 return (
27 <Popover>
28 <PopoverTrigger asChild>
29 <Button variant="ghost" size="icon" className="relative">
30 <Bell className="h-5 w-5" />
31 {unread > 0 && <Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center">{unread}</Badge>}
32 </Button>
33 </PopoverTrigger>
34 <PopoverContent className="w-80">
35 <ScrollArea className="h-80">
36 {notifications.map(n => (
37 <div key={n.id} className={`p-3 border-b ${n.is_read ? '' : 'bg-muted/50'}`}>
38 <p className="text-sm font-medium">{n.title}</p>
39 <p className="text-xs text-muted-foreground">{n.body}</p>
40 </div>
41 ))}
42 </ScrollArea>
43 </PopoverContent>
44 </Popover>
45 )
46}

Expected result: The bell shows unread count and streams new notifications in real-time. Clicking opens a dropdown with the notification list.

4

Build preference settings and deploy

Create the preferences page where users toggle notifications per category and channel. Add push subscription registration and deploy.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create notification settings at app/settings/notifications/page.tsx.
3// Requirements:
4// - Fetch all notification categories from templates (distinct categories)
5// - For each category, show a row with: category name, three Switch toggles (In-App, Email, Push)
6// - Pre-fill with current user preferences from notification_preferences
7// - Save Button uses Server Action to upsert preferences per category
8// - Push toggle: if enabling push for the first time, trigger browser permission request and register service worker
9// - Store push subscription in push_subscriptions table via /api/notifications/push/subscribe
10// - Use shadcn/ui Switch, Table layout, Separator between categories, Toast for save confirmation
11// Also configure NEXT_PUBLIC_VAPID_PUBLIC_KEY in Vars for service worker registration and VAPID_PRIVATE_KEY without prefix for server-side push.

Pro tip: Set RESEND_API_KEY and VAPID_PRIVATE_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix. Set NEXT_PUBLIC_VAPID_PUBLIC_KEY with the prefix for the service worker.

Expected result: Users can toggle notification channels per category. Push subscription is registered when enabled. The app is deployed.

Complete code

app/api/notifications/send/route.ts
1import { createClient } from '@supabase/supabase-js'
2import { NextRequest, NextResponse } from 'next/server'
3import { Resend } from 'resend'
4
5const supabase = createClient(
6 process.env.SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9const resend = new Resend(process.env.RESEND_API_KEY)
10
11function render(tpl: string, data: Record<string, string>) {
12 return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => data[k] || '')
13}
14
15export async function POST(req: NextRequest) {
16 const { user_id, template_slug, data } = await req.json()
17
18 const { data: tpl } = await supabase
19 .from('notification_templates')
20 .select('*')
21 .eq('slug', template_slug)
22 .single()
23
24 if (!tpl) {
25 return NextResponse.json({ error: 'Template not found' }, { status: 404 })
26 }
27
28 const title = render(tpl.title_template, data)
29 const body = render(tpl.body_template, data)
30
31 const { data: prefs } = await supabase
32 .from('notification_preferences')
33 .select('*')
34 .eq('user_id', user_id)
35 .eq('category', tpl.category)
36 .single()
37
38 const p = prefs || { in_app: true, email: true, push: false }
39 const sent: string[] = []
40
41 if (p.in_app) {
42 sent.push('in_app')
43 await supabase.from('notifications').insert({
44 user_id, template_id: tpl.id,
45 title, body, data, channels_sent: sent,
46 })
47 }
48
49 if (p.email) {
50 sent.push('email')
51 const { data: u } = await supabase.auth.admin.getUserById(user_id)
52 if (u?.user?.email) {
53 await resend.emails.send({
54 from: 'noreply@yourdomain.com',
55 to: u.user.email,
56 subject: title,
57 html: `<p>${body}</p>`,
58 })
59 }
60 }
61
62 return NextResponse.json({ success: true, channels: sent })
63}

Customization ideas

Notification digest emails

Send a daily or weekly digest email summarizing unread notifications instead of individual emails for each event.

Slack channel integration

Add Slack as a notification channel by posting to a Slack webhook URL when critical notifications are triggered.

Notification center page

Build a full-page notification history with filters by category, channel, and read status, beyond the bell dropdown.

Batch notification sending

Add a bulk send API that dispatches the same notification to multiple users at once, useful for product announcements.

Common pitfalls

Pitfall: Using Promise.all instead of Promise.allSettled for multi-channel dispatch

How to avoid: Use Promise.allSettled which resolves with results for all promises regardless of individual failures. Log failed channels for retry.

Pitfall: Not cleaning up Realtime subscriptions on component unmount

How to avoid: Return a cleanup function from useEffect that calls supabase.removeChannel(channel) to properly unsubscribe.

Pitfall: Storing VAPID_PRIVATE_KEY with NEXT_PUBLIC_ prefix

How to avoid: Store VAPID_PRIVATE_KEY without prefix in V0's Vars tab. Only NEXT_PUBLIC_VAPID_PUBLIC_KEY should have the prefix.

Best practices

  • Use Promise.allSettled for multi-channel dispatch to handle partial failures gracefully
  • Clean up Supabase Realtime subscriptions in useEffect cleanup to prevent memory leaks
  • Store templates as JSONB with variable placeholders ({{name}}, {{action}}) for reusable notification content
  • Set RESEND_API_KEY and VAPID_PRIVATE_KEY without NEXT_PUBLIC_ prefix in V0's Vars tab
  • Use Supabase Realtime for instant in-app notification delivery without polling
  • Let users control preferences per category and channel with granular Switch toggles
  • Use V0's Design Mode (Option+D) to adjust the notification bell Popover width and notification card styling

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a multi-channel notification system with Next.js, Supabase, and Resend. I need an API route that takes a user_id, template_slug, and data object. It should render the template by replacing {{variable}} placeholders, check user preferences, and dispatch to enabled channels (in-app via Supabase insert, email via Resend, push via web-push). Use Promise.allSettled for dispatch. Please write the route.

Build Prompt

Create a notification template rendering engine. Templates use {{variable}} syntax in title and body. The render function receives a template string and a data object, replacing all {{key}} with corresponding values. Then build the dispatch function that checks notification_preferences for the user and category, and sends to enabled channels using Promise.allSettled.

Frequently asked questions

How do real-time in-app notifications work?

When the send API inserts a notification into the database, Supabase Realtime detects the change and pushes it to all connected clients subscribed to that user's notifications channel. The bell component updates instantly without polling.

What happens if one notification channel fails?

Promise.allSettled ensures all channels are attempted regardless of individual failures. If email fails but in-app succeeds, the user still gets the in-app notification. Failed channels are logged for debugging.

How do push notifications work?

When a user enables push in preferences, the browser requests permission and registers a service worker. The push subscription (endpoint + keys) is stored in Supabase. When sending, the server uses the web-push library with VAPID keys to deliver the notification to the browser.

Do I need a paid V0 plan?

Yes, Premium ($20/month) at minimum. The notification system has many components (bell, preferences, dispatch API, push setup) requiring multiple prompts.

Can users disable notifications for specific categories?

Yes. The preferences page shows Switch toggles per category (e.g., billing, marketing, security) and per channel (in-app, email, push). Users can fine-tune exactly what they receive and how.

How do I deploy the notification system?

Click Share in V0, then Publish to Production. Set RESEND_API_KEY, VAPID_PRIVATE_KEY (no prefix), and NEXT_PUBLIC_VAPID_PUBLIC_KEY in the Vars tab. Enable Realtime on the notifications table in Supabase Dashboard.

Can RapidDev help build a custom notification system?

Yes. RapidDev has built over 600 apps including notification systems with multi-channel delivery, digest scheduling, and analytics dashboards. Book a free consultation to discuss your notification requirements.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.