Actionable Notifications
Some notifications carry metadata that enables inline Accept/Decline actions directly from the notification UI, without navigating to a separate page.
How It Works
Actionable Notification Types
| Notification Type | Metadata Field | Frontend Action |
|---|---|---|
INVITATION_RECEIVED | metadata.token | acceptCollaborationInvitation(token) / declineCollaborationInvitation(token) |
CONNECTION_REQUEST_RECEIVED | metadata.requestId | acceptCompanyConnectionRequestById(requestId) / declineCompanyConnectionRequestById(requestId) |
Backend: Passing Metadata
The createFromTemplate() method accepts an optional fourth parameter for metadata, resource tracking, and company scoping:
await this.notificationService.createFromTemplate(
userId, // recipient
NotificationType.INVITATION_RECEIVED, // type (determines template)
{ // template variables (for title/message rendering)
invitationType: InvitationType.COLLABORATOR,
companyName: 'Tech Corp',
roleName: 'Recruiter',
inviterName: 'John Doe',
invitationId: 'invite-123',
invitationLink: 'https://...',
personalMessage: '',
},
{ // options (stored on notification record)
companyId: 'company-123',
resourceType: 'COLLABORATION_INVITATION',
resourceId: 'invite-123',
metadata: {
token: 'abc123def456', // frontend reads this for accept/decline
},
},
);
Key Distinction
- Template variables (3rd param) — Used to render
{{variable}}placeholders in title/message templates. Not stored as-is. - Options (4th param) — Stored directly on the notification record. The
metadataJSON field is exposed to the frontend via GraphQL.
Frontend: Reading Metadata
The frontend checks notification.metadata to determine if inline actions should render:
// notificationActions.ts
function getNotificationActionContext(notification) {
if (notification.type === 'CONNECTION_REQUEST_RECEIVED') {
const requestId = notification.metadata?.requestId;
if (!requestId) return null;
return { actionType: 'CONNECTION_REQUEST', requestId };
}
if (notification.type === 'INVITATION_RECEIVED') {
const token = notification.metadata?.token;
if (!token) return null;
return { actionType: 'COLLABORATION', token };
}
return null;
}
Buttons only render when:
- Notification status is
UNREAD - Notification type is actionable
- Required metadata field is present
Adding New Actionable Types
To make a new notification type actionable:
- Backend listener — Pass the required identifier in
options.metadatawhen callingcreateFromTemplate()orcreateNotification() - Frontend
notificationActions.ts— Add a new case ingetNotificationActionContext()that extracts the identifier from metadata - Frontend
useNotificationActions.ts— Add the accept/decline mutation calls for the new action type - Frontend
NotificationActions.tsx— No changes needed (generic component)
Event Payloads
invitation.received
{
invitationId: string;
invitedUserId: string;
invitedEmail: string;
invitedByUserId: string;
companyId: string;
roleName: string;
invitationLink: string; // full URL with token
token: string; // invitation token (used in metadata)
message?: string;
}
connection.request.received
{
requestId: string; // connection request ID (used in metadata)
requesterCompanyId: string;
targetCompanyId: string;
requestMessage?: string;
}
Security
Notification metadata is only accessible to the notification's owner (userId). All notification queries are scoped by the authenticated user's ID — there is no way to read another user's notification metadata.
The invitation token in metadata is the same token that would be embedded in the actionUrl link. Exposing it in metadata does not increase the attack surface since the user already has access to the same token via the notification's actionUrl field.