# Stacks Notifications

> Use when implementing notifications in Stacks - multi-channel notifications (email, SMS, push, chat, database), the database notification driver with read/unread tracking, notification factories (useEmail, useSMS, useChat, useDatabase), or notification configuration. Covers @stacksjs/notifications and config/notification.ts.

- Skill: `stacksjs/stacks-notifications` (Agent Skill)
- Install (CLI): `npx skillmds@latest add stacksjs/stacks-notifications`
- Raw SKILL.md: https://api.skillmd.com/api/skills/stacksjs/stacks-notifications/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- License: MIT
- Author: stacksjs (https://skillmd.com/u/stacksjs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/stacksjs/stacks-notifications

---


# 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

```typescript
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.

```typescript
// 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:
- `email` driver: `@stacksjs/email`
- `sms` driver: `@stacksjs/sms`
- `chat` driver: `@stacksjs/chat`
- `push` driver: `@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

```typescript
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_id` from `options.userId`
- `type` from `options.type`
- `data` -- JSON.stringify'd from `options.data`
- `read_at` set to `null`
- `created_at` and `updated_at` set to current ISO timestamp

### Query Notifications

```typescript
// 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

```typescript
// 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

```typescript
// Delete a single notification by ID
await db.deleteNotification(notificationId)

// Delete all notifications for a user
await db.deleteAllNotifications(userId)
```

### DatabaseNotification Interface

```typescript
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

```typescript
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_id` comes from the `belongsTo: ['User']` relationship
- `type` is an application event name such as `order.shipped`
- `data` is a JSON string containing the notification payload
- `readAt` maps to the nullable `read_at` column
- `created_at` and `updated_at` come from `useTimestamps`

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

```typescript
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 via `config/email.ts`)
- **SMS channel** (`useSMS`) -- re-exports `@stacksjs/sms` (configured via `config/sms.ts`)
- **Chat channel** (`useChat`) -- re-exports `@stacksjs/chat` (configured via `config/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 `data` field in database notifications is stored as a JSON string -- always `JSON.parse()` when reading
- `read_at` is `null` for unread notifications -- use this to filter unread
- The database driver uses Kysely's query builder with `as any` type 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 if `config.default` is not set in `config/notification.ts`
- The `notification()` function (without arguments) is a shorthand for `useNotification()` with defaults
- The `nexmo` driver is a legacy alias for `vonage` in the SMS drivers index

