Notifications
Overview
The notification system delivers in-app notifications to users with read/unread tracking and actionable items. Notifications are user-scoped (not account-scoped), supporting typed messages with optional actions such as accepting or declining team invites.
Each notification carries a type, a title, an optional message, arbitrary data (JSONB), and an optional actions array defining available user actions. Notifications track both read_at and acted_at timestamps to distinguish between viewed and resolved states.
Architecture
Dashboard UI
|
v
Next.js API Proxy (/api/notifications)
|
v
GraphQL (NotificationQuery / NotificationMutation)
|
v
db::notifications (PostgreSQL)
Notifications are created server-side (e.g., when a team invite is sent) and consumed by the frontend via GraphQL queries. The notification list includes an unread_count field for badge display without fetching all items.
Actionable Notifications
Some notifications include an actions array in JSONB format:
[
{ "action": "accept_invite", "label": "Accept" },
{ "action": "decline_invite", "label": "Decline" }
]
When a user executes an action, the system validates the action exists in the notification's actions array, then dispatches to the appropriate handler. Currently supported actions:
accept_invite-- Accepts a team invite by looking updata.inviteId, retrieving the invite, and callingdb::members::accept_inviteto add the user as an account member with the invited role.decline_invite-- Marks the notification as acted upon without side effects.
After action execution, the notification is marked with acted_at and read_at (if not already read).
API
GraphQL Queries
| Query | Args | Returns | Permission |
|---|---|---|---|
notifications | limit?: Int, offset?: Int | NotificationList | Auth only |
unreadNotificationCount | -- | Int | Auth only |
NotificationList includes:
items: [Notification!]!-- Paginated list ordered bycreated_at DESCtotal: Int!-- Total notification count for paginationunreadCount: Int!-- Count of unread notifications
Default limit is 25, max is 100.
GraphQL Mutations
| Mutation | Args | Returns | Permission |
|---|---|---|---|
markNotificationRead | id: UUID | Notification | Auth only |
markAllNotificationsRead | -- | MarkAllReadResult | Auth only |
executeNotificationAction | id: UUID, action: String | Notification | Auth only |
All mutations verify user ownership -- a user can only interact with their own notifications.
REST Endpoints
All paths live under /v1. Notifications are user-scoped — authentication is enough, no resource:action guard.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/notifications | Auth | Paginated list (supports limit, offset) with unread_count |
PATCH | /v1/notifications/{id}/read | Auth | Mark a notification as read |
POST | /v1/notifications/{id}/action | Auth | Execute a notification action (e.g., accept_invite, decline_invite) |
POST | /v1/notifications/read-all | Auth | Mark all notifications as read |
GraphQL Types
type Notification {
id: UUID!
userId: UUID!
type: String!
title: String!
message: String
data: JSON!
actions: JSON
readAt: String
actedAt: String
createdAt: String!
}
type NotificationList {
items: [Notification!]!
total: Int!
unreadCount: Int!
}
type MarkAllReadResult {
updated: Int!
}
Permissions
Notifications require authentication only (no account-level permissions). Any logged-in user can view and manage their own notifications. Ownership is enforced by the resolvers (user_id check).
Database
Table: notifications
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Notification ID |
user_id | UUID (FK) | Target user |
type | TEXT | Notification type (e.g., invite) |
title | TEXT | Display title |
message | TEXT | Optional detail message |
data | JSONB | Arbitrary payload (e.g., { "inviteId": "..." }) |
actions | JSONB | Optional array of available actions |
read_at | TIMESTAMPTZ | When marked as read |
acted_at | TIMESTAMPTZ | When an action was executed |
created_at | TIMESTAMPTZ | Creation timestamp |
DB Functions
| Function | Description |
|---|---|
list_notifications | Paginated list for a user, ordered by created_at DESC |
count_notifications | Total count for a user |
count_unread | Count where read_at IS NULL |
get_notification | Single notification by ID |
mark_as_read | Sets read_at = now() |
mark_all_read | Bulk update all unread for a user |
mark_acted | Sets acted_at = now() and read_at = COALESCE(read_at, now()) |
create_notification | Insert a new notification |
delete_notification | Delete by ID |
Key Files
| File | Purpose |
|---|---|
apps/api/src/graphql/notifications.rs | GraphQL queries, mutations, and action dispatch |
apps/api/src/db/notifications.rs | Database CRUD operations |
crates/lo-auth/src/rbac.rs | Permission constants (notifications removed — auth only) |