Stacks Notifications
Multi-channel notification system with 5 channel types: email, SMS, chat, push, and database.
Key Paths
- Core package:
storage/framework/core/notifications/src/ - Main entry:
storage/framework/core/notifications/src/index.ts - Drivers:
storage/framework/core/notifications/src/drivers/ - Database driver:
storage/framework/core/notifications/src/drivers/database.ts - Configuration:
config/notification.ts - Notification model:
storage/framework/defaults/app/Models/Notification.ts
Package Exports
import {
useChat,
useEmail,
useSMS,
useDatabase,
useNotification,
notification,
DatabaseNotificationDriver,
} from '@stacksjs/notifications'
// Types
import type { CreateNotificationOptions, DatabaseNotification } from '@stacksjs/notifications'
Channel Factories
Each factory returns the underlying driver module for that channel.
// Email -- defaults to 'mailtrap', returns @stacksjs/email driver
const emailDriver = useEmail('ses')
const emailDriver = useEmail('sendgrid')
const emailDriver = useEmail('mailgun')
const emailDriver = useEmail('mailtrap')
const emailDriver = useEmail('smtp')
// SMS -- defaults to 'twilio', returns @stacksjs/sms driver
const smsDriver = useSMS('twilio')
const smsDriver = useSMS('vonage')
// Chat -- defaults to 'slack', returns @stacksjs/chat driver
const chatDriver = useChat('slack')
const chatDriver = useChat('discord')
const chatDriver = useChat('teams')
// Database -- returns DatabaseNotificationDriver object
const dbDriver = useDatabase()
// Auto-detect by type -- dispatches to the correct factory
const driver = useNotification('email', 'ses')
const driver = useNotification('sms', 'twilio')
const driver = useNotification('chat', 'slack')
const driver = useNotification('database')
// Default (uses config.default, falls back to 'email' + 'mailtrap')
const driver = notification()
useNotification() reads config/notification.ts for the default type. If no default is set, it throws 'No default notification type set in config/notification.ts'.
The channel drivers are re-exports from their respective packages:
emaildriver:@stacksjs/emailsmsdriver:@stacksjs/smschatdriver:@stacksjs/chatpushdriver:@stacksjs/push
Database Notification Driver
The DatabaseNotificationDriver provides CRUD operations for notifications stored in the notifications database table using Kysely query builder.
Send a Notification
const db = useDatabase()
const notification = await db.send({
userId: 1,
type: 'order.shipped',
data: { orderId: 42, trackingNumber: 'ABC123' },
})
// Returns the created DatabaseNotification with auto-generated id, timestamps
send() inserts a row into the notifications table with:
user_idfromoptions.userIdtypefromoptions.typedata-- JSON.stringify'd fromoptions.dataread_atset tonullcreated_atandupdated_atset to current ISO timestamp
Query Notifications
// All notifications for a user, ordered by created_at desc
const all = await db.getUserNotifications(userId)
// Only unread (where read_at is null), ordered by created_at desc
const unread = await db.getUnreadNotifications(userId)
// Count of unread notifications
const count = await db.unreadCount(userId)
Mark as Read
// Mark a single notification as read (sets read_at to current timestamp)
await db.markAsRead(notificationId)
// Mark all unread notifications for a user as read
await db.markAllAsRead(userId)
markAllAsRead() only updates rows where read_at is null.
Delete Notifications
// Delete a single notification by ID
await db.deleteNotification(notificationId)
// Delete all notifications for a user
await db.deleteAllNotifications(userId)
DatabaseNotification Interface
interface DatabaseNotification {
id: number
user_id: number
type: string // e.g., 'order.shipped', 'payment.received'
data: string // JSON stringified -- parse with JSON.parse() when reading
read_at: string | null // ISO timestamp or null if unread
created_at: string // ISO timestamp
updated_at: string | null // ISO timestamp
}
CreateNotificationOptions Interface
interface CreateNotificationOptions {
userId: number
type: string
data: Record<string, any>
}
Notification Model Fields
The Notification model at storage/framework/defaults/app/Models/Notification.ts
maps the database notification inbox used by DatabaseNotificationDriver:
user_idcomes from thebelongsTo: ['User']relationshiptypeis an application event name such asorder.shippeddatais a JSON string containing the notification payloadreadAtmaps to the nullableread_atcolumncreated_atandupdated_atcome fromuseTimestamps
The model uses useApi for its CRUD API and seeds 30 records by default. It
must stay aligned with notificationsTableSql() in
storage/framework/core/database/src/notification-tables.ts. Outbound
transport attempts belong in a separate delivery-log model and table. Do not
add email, SMS, or provider-specific delivery columns to the inbox model.
CLI Commands
buddy make:notification [name]-- scaffold a new notification
config/notification.ts
import type { NotificationConfig } from '@stacksjs/types'
export default {
default: 'email',
} satisfies NotificationConfig
The default field controls which channel type useNotification() and notification() use when no type is specified. Valid values: 'email', 'sms', 'chat', 'database'.
Architecture
The notifications package is a thin aggregation layer. Each channel delegates to its own dedicated package:
- Email channel (
useEmail) -- re-exports@stacksjs/email(configured viaconfig/email.ts) - SMS channel (
useSMS) -- re-exports@stacksjs/sms(configured viaconfig/sms.ts) - Chat channel (
useChat) -- re-exports@stacksjs/chat(configured viaconfig/services.ts) - Push channel -- re-exports
@stacksjs/push - Database channel (
useDatabase) -- built-in driver using@stacksjs/database
The driver modules (drivers/email.ts, drivers/sms.ts, etc.) are single-line re-exports: export * as email from '@stacksjs/email', export * as sms from '@stacksjs/sms', etc.
Gotchas
- The
datafield in database notifications is stored as a JSON string -- alwaysJSON.parse()when reading read_atisnullfor unread notifications -- use this to filter unread- The database driver uses Kysely's query builder with
as anytype casts on table/column names since the notifications table is dynamically referenced - Channel-specific configuration (SMTP credentials, Twilio keys, Slack tokens) lives in each channel's own config file, not in
config/notification.ts useNotification()throws ifconfig.defaultis not set inconfig/notification.ts- The
notification()function (without arguments) is a shorthand foruseNotification()with defaults - The
nexmodriver is a legacy alias forvonagein the SMS drivers index