Notifications GraphQL API
Real-time notification system with queries, mutations, and WebSocket subscriptions.
Notification Targeting
Notifications support three targeting modes:
| Mode | Condition | Audience |
|---|---|---|
| Direct | targetUserId is set | Single user |
| Role-based | targetUserId is null, targetRoles is set | All users whose groups grant any of the specified roles |
| Broadcast | Both targetUserId and targetRoles are null | All active users in the organization |
targetUserId and targetRoles are mutually exclusive — setting both is a validation error. targetRoles must not be an empty list.
Read Status
Read status (isRead, readAt) is tracked per user via a UserNotification junction table. UserNotification rows are created lazily — only when a user marks a notification as read. Until then, the notification appears as unread.
Entity Links in the UI
When a notification includes both entityType and entityId, the web UI attempts to open the matching app route as a dialog. It builds a path in the form entityType/entityId, checks the matched route's permission, permissions, or requiredPermissions, and passes matched route parameters plus organizationId and currentUser into the dialog component. If no route exists or the user lacks permission, the UI shows an error instead of opening the item.
entityId is a String, not an Int — it needs to hold any entity's id, including a GUID such as an agent session's agentSessionId, not just an integer primary key. See Breaking change below.
Queries
getNotification
Fetch a single notification for the current user.
query {
getNotification(organizationId: 1, notificationId: 42) {
notificationId
title
message
type
priority
targetUserId
targetRoles
entityType
entityId
isRead
readAt
created
}
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
organizationId | Int | Yes | Organization scope |
notificationId | Int | Yes | Notification to fetch |
Returns null if the notification doesn't exist or isn't visible to the current user (see Notification Targeting).
getNotifications
Paginated list of notifications for the current user. Supports filtering, searching, and sorting.
query {
getNotifications(organizationId: 1, filter: "isRead:false", orderBy: "-created", skip: 0, take: 20) {
items {
notificationId
title
message
type
priority
targetRoles
isRead
created
}
totalCount
}
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
organizationId | Int | Yes | Organization scope |
filter | String | No | Lucene filter syntax (e.g. isRead:false, type:Alert) |
search | String | No | Full-text search |
orderBy | String | No | Sort field (default: -created = newest first) |
getUnreadNotificationCount
Returns the count of unread notifications for the current user.
query {
getUnreadNotificationCount(organizationId: 1)
}
Mutations
createNotification
Create a new notification. Targeting is determined by targetUserId and targetRoles — see Notification Targeting.
mutation {
createNotification(organizationId: 1, values: {
title: "Order #1234 shipped"
message: "Your order has been picked up by the carrier."
type: ORDER_UPDATE
priority: NORMAL
entityType: "Order"
entityId: "1234"
}) {
notificationId
title
created
}
}
Input fields (CreateNotificationCommandValues):
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | String | Yes | — | Notification title |
message | String | No | — | Notification body (supports Markdown) |
type | NotificationType | Yes | — | System, OrderUpdate, TaskAssignment, Alert, Info |
priority | NotificationPriority | No | Normal | Low, Normal, High, Urgent |
targetUserId | String | No | — | Target a specific user (mutually exclusive with targetRoles) |
targetRoles | [String] | No | — | Target users by role names (mutually exclusive with targetUserId; must not be empty if provided) |
entityType | String | No | — | Linked entity type name |
entityId | String | No | — | Linked entity ID. Any entity's id as a string — an integer primary key ("1234") or a GUID, such as an agent session's agentSessionId. |
expiresAt | DateTime | No | — | Optional expiration |
Breaking change: entityId is a String
entityId (on notification and on createNotification's values) changed from Int to String, so a notification can link to an entity whose id isn't an integer — such as entityType: "AgentSession", whose tool-approval notification carries a GUID entityId. Clients that parsed entityId as a number, or sent an integer literal for it, need to switch to plain string handling.
markNotificationRead
Mark a single notification as read for the current user.
mutation {
markNotificationRead(organizationId: 1, notificationId: 42)
}
Returns true on success.
markAllNotificationsRead
Mark all notifications as read for the current user. Returns the count of notifications marked.
mutation {
markAllNotificationsRead(organizationId: 1)
}
deleteNotification
Delete a notification (removes for all users).
mutation {
deleteNotification(organizationId: 1, notificationId: 42) {
deletedCount
id
}
}
Subscriptions
onNotificationReceived
Real-time WebSocket subscription for new notifications. Uses PostgreSQL NOTIFY/LISTEN under the hood.
subscription {
onNotificationReceived(organizationId: 1, userId: "user-abc-123") {
notificationId
title
message
type
priority
entityType
entityId
isRead
created
createdBy
}
}
Topic format: {organizationId}_{userId}_notifications
WebSocket Authentication
GraphQL WebSocket subscriptions authenticate during connection_init. Send the access token in connection parameters as either a raw token or a Bearer header value:
{ "Authorization": "Bearer <access-token>" }
Connections without a token, with an empty token, or with a token that fails OpenIddict validation are rejected. The server also enforces a 10-second initialization timeout for clients that open a socket but never send connection_init.
For notification subscriptions, the userId argument must exactly match the authenticated user ID. Subscribing to another user's notification topic returns a FORBIDDEN GraphQL error.
The subscription fires whenever a new notification is created that targets the specified user (either directly or via broadcast).
Frontend Integration
The frontend uses the useNotifications React hook which:
- Fetches notifications via
GetNotificationsquery (RTK Query) - Fetches unread count via
GetUnreadNotificationCountquery - Subscribes to
onNotificationReceivedviagraphql-wsWebSocket client - Shows toast notifications for real-time updates
- Provides
markAsRead,markAllAsRead, anddeleteNotificationactions
The NotificationsDropdown component in the navbar renders the notification bell icon with unread badge and a dropdown panel with the notification list. Message content is rendered as Markdown.
Mobile push device registration
Native iOS and Android builds register an Expo push token after both authentication and organization selection are available. Registration is repeated when the selected organization changes or Expo rotates the token, while unchanged token/organization pairs are deduplicated. Push permission denial and token-registration failures do not block login.
mutation RegisterUserDevice($input: RegisterUserDeviceCommandInput!) {
registerUserDevice(input: $input) {
userDeviceId
token
platform
appId
appVersion
}
}
mutation UnregisterUserDevice($input: UnregisterUserDeviceCommandInput!) {
unregisterUserDevice(input: $input)
}
Registration values include the Expo token, Ios or Android platform, white-label application ID, and app version, scoped by organizationId. Normal logout unregisters the token before clearing the authenticated session so shared driver devices do not continue receiving the previous user's notifications. If token refresh has already invalidated the session, logout skips the network unregister to avoid a refresh deadlock. Unregister waits at most five seconds and always clears the local registration state.