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:

LayerPurposeLocation
DatabasePersistent storage with RLSpublic.notifications table
ServerSend notifications from backend@kit/notifications/server
ClientDisplay 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,
});
}

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 hours
await 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 action
await service.createNotification({
accountId,
body: 'New comment on your post',
link: '/posts/123#comment-456',
});
// Less useful: No action
await 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 hour
const urgentExpiry = new Date(Date.now() + 60 * 60 * 1000);
// General: Expires in 7 days
const generalExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

Notifications documentation