๐Ÿ””

Notification System

Real-time notificaties via Socket.IO, Web Push / Service Worker (PWA), badge counts en RBAC-gestuurde notificatietypen.

Fastify 5 PostgreSQL 16 + Drizzle ORM React 19 + TanStack Query v5 Socket.IO Web Push API

๐Ÿ“ฌ Notification Types

Alle notificatietypen, RBAC-gestuurd. Elke type heeft een eigen Socket.IO event en push payload.

๐Ÿ’ธ
Tip Received
Creator only. Bedrag + sender. Badge +1.
๐Ÿ‘ค
New Follower
Alle rollen. Follower-avatar + naam.
๐Ÿ’ฌ
New Message
Alle rollen. Sender + preview (max 60 chars).
๐ŸŽฅ
Stream Started
Volgers van creator. Push + in-app.
๐Ÿ†
Achievement Unlocked
Alle rollen. Badge naam + XP reward.
๐Ÿ”“
Content Unlocked
Creator only. Content ID + koper.
โš™๏ธ
System Alert
Maintenance, policy update, account action.
๐Ÿ›ก๏ธ
Moderation Alert
Admin only. Content report, DMCA, ban trigger.

๐Ÿ” RBAC โ€“ Notificaties per Rol

Notificatietype Member Creator Admin
๐Ÿ’ธ Tip Receivedโ€“โœ“โœ“
๐Ÿ‘ค New Followerโœ“โœ“โœ“
๐Ÿ’ฌ New Messageโœ“โœ“โœ“
๐ŸŽฅ Stream Startedโœ“โœ“โœ“
๐Ÿ† Achievement Unlockedโœ“โœ“โœ“
๐Ÿ”“ Content Unlockedโ€“โœ“โœ“
โš™๏ธ System Alertโœ“โœ“โœ“
๐Ÿ›ก๏ธ Moderation Alertโ€“โ€“โœ“

โšก Real-time Delivery โ€” Socket.IO

Events

socket.emit โ†’
notification:new

Server emits to user room. Payload: { id, type, actor, message, createdAt }

socket.on โ†’
notification:read

Client emits na lezen. Server markeert als read, update badge count.

Server-side (Fastify 5)

// Fastify plugin: notification emitter
fastify.post('/notifications/emit', async (req) => {
  const { userId, type, payload } = req.body;
  io.to(`user:${userId}`).emit('notification:new', {
    id: crypto.randomUUID(),
    type,
    ...payload,
    createdAt: new Date(),
  });
  await db.insert(notifications).values({
    userId, type, data: payload,
  });
});

๐Ÿ“ฒ Push Notifications โ€” Web Push API / PWA

Service Worker registratie

// main.tsx โ€“ SW registratie (VitePWA)
if ('serviceWorker' in navigator) {
  const reg = await navigator.serviceWorker
    .register('/sw.js');
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: VAPID_PUBLIC_KEY,
  });
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(sub),
  });
}

Push vanuit Fastify

import webpush from 'web-push';
webpush.setVapidDetails(
  'mailto:support@teezu.app',
  VAPID_PUBLIC,
  VAPID_PRIVATE
);

// Stuur push naar subscriber
await webpush.sendNotification(subscription, JSON.stringify({
  title: '๐Ÿ’ธ Tip ontvangen!',
  body: 'LovelyLena stuurde 50 tokens',
  icon: '/icons/icon-192.png',
  data: { url: '/wallet' },
}));
๐Ÿ“Œ Service Worker flow: PWA registreert SW โ†’ vraagt push permission โ†’ stuurt VAPID subscription naar POST /api/push/subscribe โ†’ server slaat op in push_subscriptions tabel (PostgreSQL) โ†’ bij event: Fastify haalt subscription op, roept webpush.sendNotification() aan.

๐Ÿ”ด Badge Count โ€” Bottom Nav & Sidebar

UI: Badge in navigatie

๐Ÿ 
Home
๐Ÿ”
Discover
๐Ÿ”” 3
Notifs
๐Ÿ’ฌ 7
Chat
๐Ÿ‘ค
Profile

Badge verdwijnt na markeren als gelezen via PATCH /notifications/:id/read of via Socket.IO notification:read event.

TanStack Query v5 hook

// useUnreadCount.ts
export function useUnreadCount() {
  return useQuery({
    queryKey: ['notifications', 'unread'],
    queryFn: () =>
      fetch('/api/notifications?read=false&limit=1')
        .then(r => r.json())
        .then(d => d.totalUnread),
    refetchInterval: 30_000,  // polling fallback
    staleTime: 10_000,
  });
}
// Socket.IO update: queryClient.invalidateQueries(...)
// na ontvangen 'notification:new' event

๐Ÿ› ๏ธ API Endpoints

GET /api/notifications Paginated lijst van notificaties voor ingelogde user
Query params
?page=1&limit=20&read=false&type=tip
Response
{ data:[], total, totalUnread, page, limit }
PATCH /api/notifications/:id/read Markeer รฉรฉn notificatie als gelezen
// Drizzle ORM
await db.update(notifications)
  .set({ readAt: new Date() })
  .where(and(
    eq(notifications.id, id),
    eq(notifications.userId, req.user.id)
  ));
DELETE /api/notifications/:id Verwijder notificatie (soft delete)
204 No Content op succes. 404 als niet gevonden of niet van deze user.

๐Ÿ—„๏ธ Database Schema โ€” PostgreSQL 16 + Drizzle ORM

// schema/notifications.ts  (Drizzle ORM)
export const notifications = pgTable('notifications', {
  id:         uuid('id').defaultRandom().primaryKey(),
  userId:     uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  type:       varchar('type', { length: 64 }).notNull(),
  // types: tip_received | new_follower | new_message | stream_started
  //        achievement_unlocked | content_unlocked | system_alert | moderation_alert
  data:       jsonb('data').default({}),
  readAt:     timestamp('read_at'),
  deletedAt:  timestamp('deleted_at'),
  createdAt:  timestamp('created_at').defaultNow().notNull(),
});

export const pushSubscriptions = pgTable('push_subscriptions', {
  id:           uuid('id').defaultRandom().primaryKey(),
  userId:       uuid('user_id').notNull().references(() => users.id),
  endpoint:     text('endpoint').notNull(),
  p256dh:       text('p256dh').notNull(),
  auth:         text('auth').notNull(),
  createdAt:    timestamp('created_at').defaultNow(),
});

๐ŸŽจ Notification Center UI

In-App Toast

L
๐Ÿ’ธ Tip ontvangen!
LovelyLena stuurde je 50 tokens
nu

Notification Center Feed

๐Ÿ’ธ
Tip ontvangen โ€” 50 tokens
LovelyLena โ€ข 2 min geleden
๐ŸŽฅ
Stream gestart
SteamyViper is nu live โ€ข 15 min geleden
๐Ÿ†
Achievement: First Fan
Je hebt 10 followers bereikt โ€ข gisteren