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:
| Type | Color | Icon | Use Case |
|---|---|---|---|
info | Blue | InfoIcon | General information, system announcements |
warning | Yellow / Amber | AlertTriangleIcon | Attention needed, approaching limits |
error | Red | CircleAlertIcon | Errors, failures, critical issues |
success | Green | CheckCircleIcon | Completed 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()),
}); | Column | Type | Description |
|---|---|---|
id | text (PK) | Unique identifier generated with generateId() |
userId | text (nullable) | Target user. NULL = global notification shown to all users |
title | text | Short notification heading |
message | text | Notification body text |
type | text (enum) | One of: info, warning, error, success |
read | integer (boolean) | Whether the user has read this notification |
createdAt | integer (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(); | Prop | Type | Default | Description |
|---|---|---|---|
count | number | 0 | Number of unread notifications (displayed as badge) |
notifications | Notification[] | [] | 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
Popovercomponent - 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
/notificationspage - 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
| Action | Form Action | Description |
|---|---|---|
| Mark as read | ?/markRead | Mark a single notification as read (hidden input: id) |
| Mark all as read | ?/markAllRead | Bulk-mark all unread notifications for the current user |
| Delete | ?/delete | Delete 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/5card 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-sonnerafter 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
| File | Purpose |
|---|---|
src/lib/components/notification-bell.svelte | Bell icon with popover preview in the app header |
src/routes/(app)/notifications/+page.svelte | Full notifications list page with CRUD actions |
src/routes/(app)/notifications/+page.server.ts | Server load function and form actions (markRead, markAllRead, delete) |
src/lib/server/db/schema.ts | Drizzle schema — notifications table definition |
src/lib/server/id.ts | ID 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