Notifications

SvelteForge Admin includes a full in-app notification system built with SvelteKit form actions and Svelte 5 reactivity. Notifications support four severity types, per-user and global targeting, real-time badge counts, and bulk operations — all without any external dependencies or third-party services.

Notification Types

Each notification has a type field that determines its visual styling — icon, color, and badge appearance:

TypeColorIconUse Case
infoBlueInfoIconGeneral information, system announcements
warningYellow / AmberAlertTriangleIconAttention needed, approaching limits
errorRedCircleAlertIconErrors, failures, critical issues
successGreenCheckCircleIconCompleted actions, confirmations

Colors are applied with dark mode variants using Tailwind CSS utility classes (e.g., text-yellow-600 dark:text-yellow-400).

Database Schema

Notifications are stored in the notifications table, defined in src/lib/server/db/schema.ts using Drizzle ORM:

export const notifications = sqliteTable("notifications", {
  id: text("id").primaryKey(),
  userId: text("user_id").references(() => users.id),
  title: text("title").notNull(),
  message: text("message").notNull(),
  type: text("type", {
    enum: ["info", "warning", "error", "success"]
  }).notNull().default("info"),
  read: integer("read", { mode: "boolean" }).notNull().default(false),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .$defaultFn(() => new Date()),
});
ColumnTypeDescription
idtext (PK)Unique identifier generated with generateId()
userIdtext (nullable)Target user. NULL = global notification shown to all users
titletextShort notification heading
messagetextNotification body text
typetext (enum)One of: info, warning, error, success
readinteger (boolean)Whether the user has read this notification
createdAtinteger (timestamp)Creation timestamp (Drizzle mode: "timestamp" for Date objects)

Global notifications: When userId is NULL, the notification is shown to every user. This is useful for system-wide announcements, maintenance warnings, or feature launch notices. User-specific notifications are linked via a foreign key to the users table.

Notification Bell Component

The notification bell lives in the top navigation bar of the app layout (the header area of the sidebar shell). It is implemented as a Svelte 5 component at #lib/components/notification-bell.svelte.

Props

let {
  count = 0,
  notifications = []
}: {
  count: number;
  notifications: Notification[]
} = $props();
PropTypeDefaultDescription
countnumber0Number of unread notifications (displayed as badge)
notificationsNotification[][]Array of recent unread notifications for the popover preview

Features

  • Unread count badge — displays the count on a red circle; caps at "9+" for double digits
  • Popover preview — clicking the bell opens a popover showing the 5 most recent unread notifications via shadcn-svelte's Popover component
  • Type-specific icons — each notification in the popover shows the corresponding icon (info, warning, error, success) with color coding
  • Time-ago display — relative timestamps: "just now", "Xm ago", "Xh ago", "Xd ago"
  • "View all" footer — links to the full /notifications page
  • Empty state — shows a bell icon with "No notifications" when the list is empty

Time-Ago Helper

function timeAgo(date: Date | null) {
  if (!date) return "";
  const now = new Date();
  const diff = now.getTime() - new Date(date).getTime();
  const minutes = Math.floor(diff / 60000);
  if (minutes < 1) return "just now";
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  return `${days}d ago`;
}

Notifications Page

The full notifications management page at /notifications provides a complete CRUD interface using SvelteKit form actions with progressive enhancement.

Available Actions

ActionForm ActionDescription
Mark as read?/markReadMark a single notification as read (hidden input: id)
Mark all as read?/markAllReadBulk-mark all unread notifications for the current user
Delete?/deleteDelete a single notification (hidden input: id)

Page Features

  • Unread count header — dynamically shows "You have X unread notifications" or "All caught up" using $derived
  • Unread highlighting — unread notifications get a border-primary/30 bg-primary/5 card style with a "New" badge
  • Type icons and colors — same icon/color mapping as the bell component for visual consistency
  • Formatted timestamps — full date display using Intl.DateTimeFormat (e.g., "Mar 7, 2:30 PM")
  • Empty state — dashed border placeholder with bell icon when no notifications exist
  • Toast feedback — success and error toasts via svelte-sonner after form submissions

Server-Side Data Loading

The notifications page loads data through SvelteKit's +page.server.ts load function. The query fetches both user-specific notifications and global notifications (where userId IS NULL):

// src/routes/(app)/notifications/+page.server.ts
import { db } from "#lib/server/db/index.js";
import { notifications } from "#lib/server/db/schema.js";
import { eq, or, isNull, desc } from "drizzle-orm";

export const load: PageServerLoad = async ({ locals }) => {
  const items = await db
    .select()
    .from(notifications)
    .where(
      or(
        eq(notifications.userId, locals.user!.id),
        isNull(notifications.userId)
      )
    )
    .orderBy(desc(notifications.createdAt));

  return { notifications: items };
};

The (app) layout server also loads the unread count so the notification bell badge stays current across all protected pages without additional requests.

Creating Notifications

To create a notification from any server-side code (form actions, API routes, hooks), insert directly into the notifications table:

import { db } from "#lib/server/db/index.js";
import { notifications } from "#lib/server/db/schema.js";
import { generateId } from "#lib/server/id.js";

// User-specific notification
await db.insert(notifications).values({
  id: generateId(),
  userId: "user_abc123",
  title: "Content Published",
  message: "Your article 'Getting Started' is now live.",
  type: "success",
});

// Global notification (shown to all users)
await db.insert(notifications).values({
  id: generateId(),
  userId: null,
  title: "Scheduled Maintenance",
  message: "The platform will be down for maintenance on Sunday 2am-4am UTC.",
  type: "warning",
});

The read field defaults to false and createdAt is auto-generated by Drizzle's $defaultFn — you do not need to set either.

Svelte 5 Patterns

The notification system demonstrates several Svelte 5 reactivity patterns:

Reactive Derived State

// Computed unread count — updates automatically when data changes
const unreadCount = $derived(
  data.notifications.filter((n) => !n.read).length
);

Props with Defaults

// Svelte 5 $props() with destructured defaults
let {
  count = 0,
  notifications = []
}: {
  count: number;
  notifications: Notification[];
} = $props();

Progressive Enhancement with use:enhance

<!-- Form action with SvelteKit's progressive enhancement -->
<form method="POST" action="?/markRead" use:enhance>
  <input type="hidden" name="id" value={notification.id} />
  <Button variant="ghost" size="icon" type="submit">
    <CheckIcon class="size-4" />
  </Button>
</form>

The use:enhance directive from $app/forms prevents full-page reloads on form submission. SvelteKit automatically invalidates the page data after the action completes, so the notification list and unread count update reactively.

Effect-Based Toast Feedback

// Show toast notifications after form actions
$effect(() => {
  if (form?.message) toast.error(form.message);
  if (form?.success) toast.success("Done");
});

Security

All notification actions (mark read, delete) include authorization checks. The where clause ensures users can only modify their own notifications or global notifications:

// Only allow actions on user's own or global notifications
.where(
  and(
    eq(notifications.id, id),
    or(
      eq(notifications.userId, locals.user!.id),
      isNull(notifications.userId)
    )
  )
)

Key Files

FilePurpose
src/lib/components/notification-bell.svelteBell icon with popover preview in the app header
src/routes/(app)/notifications/+page.svelteFull notifications list page with CRUD actions
src/routes/(app)/notifications/+page.server.tsServer load function and form actions (markRead, markAllRead, delete)
src/lib/server/db/schema.tsDrizzle schema — notifications table definition
src/lib/server/id.tsID generator used for new notification records

Need More?

Real-Time Notifications with DashboardPack

Need real-time notifications with WebSocket support, push notifications, and email integration? Our premium templates include live notification streams, configurable delivery channels (in-app, email, push), notification preferences per user, and scheduled notification campaigns.

  • WebSocket-powered real-time notification delivery
  • Push notifications via Web Push API with service workers
  • Email notification digests (instant, hourly, daily)
  • Per-user notification preferences and mute controls
  • Notification templates with variable interpolation