๐ฌ 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