In-App Notifications for Next.js Supabase SaaS Applications
Build real-time in-app notifications for your SaaS application. Learn how to send, display, and manage user notifications with database-backed storage and real-time updates.
Makerkit includes a complete notification system for in-app alerts and updates. Notifications are stored in the database, displayed in the UI, and can be dismissed or marked as read by users.
Notification architecture
The notification system consists of three layers:
| Layer | Purpose | Location |
|---|---|---|
| Database | Persistent storage with RLS | public.notifications table |
| Server | Send notifications from backend | @kit/notifications/server |
| Client | Display and manage UI | @kit/notifications/client |
This architecture ensures notifications persist across sessions and can be triggered from any server context.
Database schema
Notifications are stored in the notifications table:
create table public.notifications ( id uuid primary key default gen_random_uuid(), account_id uuid not null references public.accounts(id), body text not null, link text, dismissed boolean default false, expires_at timestamp with time zone, created_at timestamp with time zone default now());Key fields:
- account_id: Links notification to a personal or team account
- body: Notification message (supports text or translation keys)
- link: Optional URL to navigate when clicked
- dismissed: Whether the user has dismissed the notification
- expires_at: Optional auto-expiration timestamp
Sending notifications
Send notifications from Server Actions, API routes, or background jobs.
Basic notification
import { createNotificationsService } from '@kit/notifications/server';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function notifyUser(accountId: string, message: string) { const client = getSupabaseServerClient(); const service = createNotificationsService(client); await service.createNotification({ accountId, body: message, });}Notification with link
Direct users to a specific page when they click the notification:
await service.createNotification({ accountId, body: 'Your document has been shared with you', link: '/documents/shared-doc-123',});Expiring notifications
Set an expiration date for time-sensitive notifications:
const expiresAt = new Date();expiresAt.setHours(expiresAt.getHours() + 24); // Expires in 24 hoursawait service.createNotification({ accountId, body: 'Special offer expires soon!', expiresAt,});Team notifications
Send notifications to all members of a team account:
async function notifyTeam(teamAccountId: string, message: string) { const client = getSupabaseServerClient(); const service = createNotificationsService(client); // This notifies the team account itself // All team members with access will see it await service.createNotification({ accountId: teamAccountId, body: message, });}Displaying notifications
The notification UI is built into Makerkit's layouts. Users see a bell icon that shows unread count and opens a dropdown with their notifications.
Notification list component
import { NotificationsList } from '@kit/notifications/client';function NotificationsPage() { return ( <div className="max-w-2xl mx-auto"> <h1>All Notifications</h1> <NotificationsList accountId={accountId} /> </div> );}Custom notification display
Access notifications directly for custom UIs:
'use client';import { useNotifications } from '@kit/notifications/client';function CustomNotificationBadge() { const { notifications, unreadCount, markAsRead } = useNotifications(); return ( <div> <span className="badge">{unreadCount}</span> {notifications.map((notification) => ( <div key={notification.id} onClick={() => markAsRead(notification.id)} > {notification.body} </div> ))} </div> );}Managing notifications
Mark as read
await service.markNotificationAsRead(notificationId);Dismiss notification
await service.dismissNotification(notificationId);Delete notification
await service.deleteNotification(notificationId);Clear all notifications
await service.clearAllNotifications(accountId);Real-time updates
Notifications support real-time updates through Supabase Realtime:
'use client';import { useEffect } from 'react';import { createBrowserClient } from '@kit/supabase/browser-client';function RealtimeNotifications({ accountId }) { useEffect(() => { const client = createBrowserClient(); const subscription = client .channel('notifications') .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications', filter: `account_id=eq.${accountId}`, }, (payload) => { // Handle new notification console.log('New notification:', payload.new); } ) .subscribe(); return () => { subscription.unsubscribe(); }; }, [accountId]); return <NotificationsList accountId={accountId} />;}Use cases
Welcome notification
Send when a user completes onboarding:
export async function completeOnboarding(userId: string) { const client = getSupabaseServerClient(); const service = createNotificationsService(client); await service.createNotification({ accountId: userId, body: 'Welcome to the platform! Start by creating your first project.', link: '/projects/new', });}Team invitation accepted
Notify team owners when someone joins:
export async function onInvitationAccepted( teamId: string, newMemberName: string) { const client = getSupabaseServerClient(); const service = createNotificationsService(client); await service.createNotification({ accountId: teamId, body: `${newMemberName} has joined your team`, link: '/settings/members', });}Subscription renewal reminder
export async function sendRenewalReminder( accountId: string, daysUntilRenewal: number) { const client = getSupabaseServerClient(); const service = createNotificationsService(client); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + daysUntilRenewal); await service.createNotification({ accountId, body: `Your subscription renews in ${daysUntilRenewal} days`, link: '/settings/billing', expiresAt, });}Best practices
1. Keep notifications actionable
Include a link when the user should take action:
// Good: Clear actionawait service.createNotification({ accountId, body: 'New comment on your post', link: '/posts/123#comment-456',});// Less useful: No actionawait service.createNotification({ accountId, body: 'Something happened',});2. Use translation keys for i18n
For internationalized apps, use translation keys:
await service.createNotification({ accountId, body: 'notifications:teamMemberJoined', link: '/settings/members',});3. Set appropriate expiration
Don't clutter users' notification lists with stale items:
// Time-sensitive: Expires in 1 hourconst urgentExpiry = new Date(Date.now() + 60 * 60 * 1000);// General: Expires in 7 daysconst generalExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);Notifications documentation
- Configuration — Setup and configuration
- Sending Notifications — Server-side notification API