let’s make the bell pop 💥 with live toast notifications. Here’s a lean MVP that’s production-safe:

Server: SQLite table + REST + SSE stream (EventSource)

Client: global Toast provider (sonner) + NotificationsProvider that listens and toasts in real time

Bell: shows unread badge; dropdown lists items

Settings: simple per-channel toggles (system, billing, project)

1) Server — Notifications (DB + REST + SSE)
1.1 Table
-- run once
CREATE TABLE IF NOT EXISTS notifications (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  message TEXT NOT NULL,
  type TEXT NOT NULL,        -- "system" | "billing" | "project"
  read INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS user_notify_prefs (
  user_id INTEGER PRIMARY KEY,
  system INTEGER DEFAULT 1,
  billing INTEGER DEFAULT 1,
  project INTEGER DEFAULT 1
);

1.2 Route + SSE broadcaster

server/routes/notifications.ts

import { Router } from "express";
import { AuthedRequest, requireAuth } from "../auth/middleware";
import { db } from "../db";
import { EventEmitter } from "events";

const router = Router();
const bus = new EventEmitter(); // broadcasts "notify:userId"

export type NotifyType = "system" | "billing" | "project";

function getPrefs(userId: number) {
  const row = db.prepare("SELECT system, billing, project FROM user_notify_prefs WHERE user_id=?").get(userId);
  return { system: row?.system ?? 1, billing: row?.billing ?? 1, project: row?.project ?? 1 };
}

/* --- REST: list + mark read --- */
router.get("/api/notifications", requireAuth, (req: AuthedRequest, res) => {
  const rows = db.prepare(
    "SELECT id, message, type, read, created_at FROM notifications WHERE user_id=? ORDER BY id DESC LIMIT 50"
  ).all(req.user!.id);
  res.json({ notifications: rows });
});

router.post("/api/notifications/read", requireAuth, (req: AuthedRequest, res) => {
  const { id } = req.body || {};
  if (!id) return res.status(400).json({ error: "id required" });
  db.prepare("UPDATE notifications SET read=1 WHERE id=? AND user_id=?").run(id, req.user!.id);
  res.json({ ok: true });
});

/* --- Settings: save prefs --- */
router.post("/api/notifications/prefs", requireAuth, (req: AuthedRequest, res) => {
  const { system = true, billing = true, project = true } = req.body || {};
  db.prepare(`
    INSERT INTO user_notify_prefs (user_id, system, billing, project)
    VALUES (?, ?, ?, ?)
    ON CONFLICT(user_id) DO UPDATE SET system=excluded.system, billing=excluded.billing, project=excluded.project
  `).run(req.user!.id, system ? 1 : 0, billing ? 1 : 0, project ? 1 : 0);
  res.json({ ok: true });
});

router.get("/api/notifications/prefs", requireAuth, (req: AuthedRequest, res) => {
  res.json(getPrefs(req.user!.id));
});

/* --- SSE stream: push new events live --- */
router.get("/api/notifications/stream", requireAuth, (req: AuthedRequest, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.flushHeaders();

  const userId = req.user!.id;
  const onMsg = (payload: any) => {
    if (payload.userId !== userId) return;
    res.write(`event: notify\n`);
    res.write(`data: ${JSON.stringify(payload)}\n\n`);
  };

  bus.on("notify", onMsg);
  req.on("close", () => bus.off("notify", onMsg));
});

/* --- Helper: create notification (use from anywhere on server) --- */
export function createNotification(userId: number, message: string, type: NotifyType = "system") {
  const prefs = getPrefs(userId);
  if (!prefs[type]) return; // respect user prefs on creation
  const info = db.prepare(
    "INSERT INTO notifications (user_id, message, type) VALUES (?,?,?)"
  ).run(userId, message, type);
  const row = db.prepare("SELECT id, message, type, read, created_at FROM notifications WHERE id=?").get(info.lastInsertRowid);
  bus.emit("notify", { userId, notification: row });
}

export default router;


Mount it:

// server/index.ts
import notifRoutes from "./routes/notifications";
app.use(notifRoutes);


Now you (or any route) can call createNotification(userId, "Your Pro plan is active!", "billing").

2) Client — Toasts + Bell + Dropdown
2.1 Global provider: listen to SSE + toast

src/notifications/NotificationsProvider.tsx

import { createContext, useContext, useEffect, useRef, useState } from "react";
import { toast } from "sonner";

type Ctx = { unread: number; refresh: () => void; items: any[] };
const NotifCtx = createContext<Ctx>({ unread: 0, refresh: () => {}, items: [] });
export const useNotifications = () => useContext(NotifCtx);

export default function NotificationsProvider({ children }: { children: React.ReactNode }) {
  const [items, setItems] = useState<any[]>([]);
  const [unread, setUnread] = useState(0);
  const evtRef = useRef<EventSource | null>(null);

  const load = async () => {
    const res = await fetch("/api/notifications", { credentials: "include" });
    const data = await res.json();
    setItems(data.notifications || []);
    setUnread((data.notifications || []).filter((n:any)=>!n.read).length);
  };

  useEffect(() => {
    load();
    const es = new EventSource("/api/notifications/stream"); // same-origin sends cookies
    evtRef.current = es;
    es.addEventListener("notify", (e: any) => {
      try {
        const payload = JSON.parse(e.data);
        const n = payload.notification;
        setItems(prev => [n, ...prev].slice(0, 50));
        setUnread(prev => prev + 1);
        // live toast
        toast(n.message, { description: new Date(n.created_at).toLocaleString() });
      } catch {}
    });
    es.onerror = () => { /* optional: retry with backoff */ };
    return () => es.close();
  }, []);

  return (
    <NotifCtx.Provider value={{ unread, refresh: load, items }}>
      {children}
    </NotifCtx.Provider>
  );
}


Wrap your app once:

// src/main.tsx or App.tsx
import { Toaster } from "sonner";
import NotificationsProvider from "@/notifications/NotificationsProvider";

function AppRoot() {
  return (
    <NotificationsProvider>
      <RouterProvider router={router} />
      <Toaster position="top-right" />
    </NotificationsProvider>
  );
}

2.2 Bell + dropdown (uses provider)

src/components/NotificationsBell.tsx

import { useState } from "react";
import { useNotifications } from "@/notifications/NotificationsProvider";

export default function NotificationsBell() {
  const [open, setOpen] = useState(false);
  const { unread, items, refresh } = useNotifications();

  const markRead = async (id: number) => {
    await fetch("/api/notifications/read", {
      method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include",
      body: JSON.stringify({ id })
    });
    refresh();
  };

  return (
    <div className="relative">
      <button className="relative" onClick={() => setOpen(v => !v)}>
        <span className="material-icons">notifications</span>
        {!!unread && <span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full" />}
      </button>

      {open && (
        <div className="absolute right-0 mt-2 w-80 bg-white border rounded-xl shadow-lg z-50 p-2 max-h-96 overflow-auto">
          {items.length ? items.map(n => (
            <div key={n.id} className={`p-2 rounded-xl mb-1 ${n.read ? "bg-gray-50" : "bg-green-50"}`}>
              <div className="text-sm">{n.message}</div>
              <div className="flex items-center justify-between mt-1">
                <div className="text-xs text-gray-500">{new Date(n.created_at).toLocaleString()}</div>
                {!n.read && (
                  <button className="text-xs underline" onClick={() => markRead(n.id)}>Mark read</button>
                )}
              </div>
            </div>
          )) : <div className="text-sm text-gray-500 p-4">No notifications yet</div>}
        </div>
      )}
    </div>
  );
}


Drop the bell where your top-right icons live:

// e.g., Header.tsx
import NotificationsBell from "@/components/NotificationsBell";
// ...
<NotificationsBell />

3) Settings (gear) — per-channel toggles

Server endpoints already added (/api/notifications/prefs GET/POST).

src/pages/Settings/NotificationsSettings.tsx

import { useEffect, useState } from "react";

export default function NotificationsSettings() {
  const [system, setSystem] = useState(true);
  const [billing, setBilling] = useState(true);
  const [project, setProject] = useState(true);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    fetch("/api/notifications/prefs", { credentials: "include" })
      .then(r=>r.json()).then(p => {
        setSystem(!!p.system); setBilling(!!p.billing); setProject(!!p.project);
      });
  }, []);

  const save = async () => {
    setSaving(true);
    await fetch("/api/notifications/prefs", {
      method: "POST", headers: {"Content-Type":"application/json"}, credentials: "include",
      body: JSON.stringify({ system, billing, project })
    });
    setSaving(false);
  };

  return (
    <div className="rounded-2xl border p-6">
      <h2 className="text-xl font-semibold mb-4">Notification Preferences</h2>

      <label className="flex items-center gap-3 mb-3">
        <input type="checkbox" checked={system} onChange={e=>setSystem(e.target.checked)} />
        System updates
      </label>
      <label className="flex items-center gap-3 mb-3">
        <input type="checkbox" checked={billing} onChange={e=>setBilling(e.target.checked)} />
        Billing alerts
      </label>
      <label className="flex items-center gap-3">
        <input type="checkbox" checked={project} onChange={e=>setProject(e.target.checked)} />
        Project activity
      </label>

      <button className="mt-5 rounded-xl bg-black text-white px-4 py-2" onClick={save} disabled={saving}>
        {saving ? "Saving…" : "Save preferences"}
      </button>
    </div>
  );
}


Add this component inside your Settings page (gear tab for “Notifications”).

4) Seed a couple of notifications (nice touch)

Right after successful checkout webhook:

// server/pay/stripe.ts (inside checkout.session.completed)
import { createNotification } from "../routes/notifications";
createNotification(userId, "Your Pro plan is active! 🎉", "billing");
createNotification(userId, "Tip: Generate your first Brand Kit to get started.", "system");


When a user creates their first project/brand:

createNotification(userId, "New project created. Let’s design your brand!", "project");

That’s it

Live toasts on new events

Bell with unread badge + dropdown

Settings with per-channel toggles (respected on creation)