# Notifications System
Real-time in-app notifications for Next.js 14 — Supabase Realtime channel, unread badge, mark-as-read, archive, bulk ops, Web Push subscribe/unsubscribe, and a bell dropdown component, all in one file.
## What's included
**Server functions (service role)**
- `createNotification(userId, type, title, body, opts?)` — inserts a notification row; auto-applies a type emoji as icon if none provided; returns the inserted row
- `createPurchaseNotification(userId, blockName)` — pre-built purchase confirmation notification linking to `/dashboard`
- `createInviteNotification(userId, workspaceName, token)` — pre-built invite notification linking to `/invite/{token}`
- `notifyAllUsers(type, title, body, actionUrl?)` — broadcasts to every user in batches of 500
- `deleteOldNotifications(olderThanDays?)` — prunes read notifications older than N days (default 90); safe to run on a cron
**Client hook**
- `useNotifications(userId)` — returns `{ items, unread, loading, markRead, markAllRead, archive, archiveAll, refresh }`; loads the 50 most recent non-archived notifications on mount; subscribes to a Supabase Realtime channel for live `INSERT` and `UPDATE` events; fires a native `Notification` on insert if browser permission is granted
**Web Push**
- `requestPushPermission()` — wraps `Notification.requestPermission()`; returns the permission state
- `subscribeToPush(userId)` — registers a service worker push subscription using `NEXT_PUBLIC_VAPID_PUBLIC_KEY`; POSTs the endpoint + keys to `/api/push/subscribe`
- `unsubscribeFromPush()` — unsubscribes the current registration and sends a `DELETE` to `/api/push/subscribe`
**UI**
- `NotificationBell` — bell button with unread badge (caps at `99+`); click opens a dropdown with the notification list, mark-all-read button, per-item archive (✕), and click-to-navigate on notifications with `action_url`; accepts `position: 'left' | 'right'` for dropdown alignment
**Types**
- `NotifType` — `'info' | 'success' | 'warning' | 'error' | 'purchase' | 'invite'`
- `AppNotification` — full notification row interface
## Setup
### 1. Install dependencies
```bash
npm install @supabase/supabase-js
```
### 2. Environment variables
```
NEXT_PUBLIC_SUPABASE_URL=your Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=anon public key
SUPABASE_SERVICE_ROLE_KEY=service role key (server-only)
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your VAPID public key # Web Push only
```
Generate VAPID keys: `npx web-push generate-vapid-keys`
### 3. Database
```sql
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
type TEXT NOT NULL DEFAULT 'info'
CHECK (type IN ('info','success','warning','error','purchase','invite')),
title TEXT NOT NULL,
body TEXT NOT NULL,
action_url TEXT,
icon TEXT,
read BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
CREATE POLICY "notif_own_select" ON notifications FOR SELECT USING (user_id::text = auth.uid()::text);
CREATE POLICY "notif_own_update" ON notifications FOR UPDATE USING (user_id::text = auth.uid()::text);
CREATE POLICY "notif_own_delete" ON notifications FOR DELETE USING (user_id::text = auth.uid()::text);
CREATE INDEX notif_user_unread_idx ON notifications(user_id, created_at DESC) WHERE read = false AND archived = false;
CREATE INDEX notif_user_all_idx ON notifications(user_id, created_at DESC) WHERE archived = false;
CREATE TABLE push_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
endpoint TEXT UNIQUE NOT NULL,
p256dh TEXT NOT NULL,
auth_key TEXT NOT NULL,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "push_own" ON push_subscriptions FOR ALL USING (user_id::text = auth.uid()::text);
```
### 4. Enable Realtime on the notifications table
In the Supabase dashboard: **Database → Replication → supabase_realtime publication → Add table → notifications**.
## Usage examples
```tsx
// Navbar — wire up the bell
'use client'
import { useNotifications, NotificationBell } from '@/blocks/notifications'
import { useSession } from 'next-auth/react'
export function Navbar() {
const { data: session } = useSession()
const notifications = useNotifications(session?.user?.id ?? null)
return (
<nav>
<NotificationBell notifications={notifications} position="right" />
</nav>
)
}
```
```ts
// After a successful purchase (webhook handler)
import { createPurchaseNotification } from '@/blocks/notifications'
await createPurchaseNotification(userId, 'Auth System')
```
```ts
// Broadcast an announcement to all users (admin route)
import { notifyAllUsers } from '@/blocks/notifications'
await notifyAllUsers('info', 'New block dropped 🎉', 'Rate Limiting is now live.', '/blocks/ratelimit')
```
## Notes
- `useNotifications` creates a new Supabase client instance on every render — extract the client creation outside the hook or use a shared instance if you render this hook in multiple components simultaneously
- The Web Push flow requires you to build `/api/push/subscribe` yourself — the block handles the browser side only; that route needs to save the subscription to `push_subscriptions` (POST) and delete it (DELETE); actually sending push messages requires a server-side `web-push` library call, which is also not included
- `notifyAllUsers` fetches all user IDs into memory before inserting — at very large user counts this will be slow and memory-hungry; move to a database function or background job for anything over ~10k users
- The bell dropdown closes only when you click the bell again — there is no click-outside-to-close handler; add a `useEffect` with a `document` click listener if you need that behaviour