@ketrics/notification-sdk
v0.3.0
Published
SDK for publishing notification events to EventBridge
Maintainers
Readme
Notification SDK
SDK for publishing notification events to AWS EventBridge within the Ketrics platform.
Overview
The Notification SDK provides a type-safe client for publishing notification events from Ketrics backend services (tenant-api, platform-api) to EventBridge. Events are consumed by the notification-lambda, which handles delivery across multiple channels (inbox, email, push).
Architecture Position
┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ tenant-api │──┐ │ │ │ │
└─────────────────────┘ │ │ EventBridge │ │ notification-lambda│
├──▶│ (event bus) │─────▶│ │
┌─────────────────────┐ │ │ │ │ - inbox-service │
│ platform-api │──┘ └─────────────────┘ │ - email-service │
└─────────────────────┘ │ - push-service │
▲ └─────────────────────┘
│ uses
┌───────┴───────────┐
│ notification-sdk │
└───────────────────┘Key Responsibilities
- Construct properly formatted notification event payloads
- Publish events to the configured EventBridge event bus
- Generate unique notification IDs for idempotency
- Abstract EventBridge API complexity from consuming services
Boundaries
This SDK only publishes events. It does not:
- Deliver notifications (handled by notification-lambda)
- Store notification state
- Query notification history
- Handle authentication/authorization (caller's responsibility)
Business Logic
Problem Solved
The platform needs to notify users about various events (ticket updates, announcements, direct messages) across multiple delivery channels. The SDK decouples notification publishing from delivery, enabling:
- Async, non-blocking notification dispatch
- Independent scaling of producers and consumers
- Consistent event schema across all sources
Core Workflows
Ticket Notifications:
- Tenant admin creates ticket → notify platform admins
- Platform admin replies → notify ticket creator
- Status changes → notify relevant parties
- Assignment changes → notify assignee
- SLA breach detected → notify platform admins (high priority)
Announcements:
- Platform admin publishes announcement
- SDK sends one event per target tenant
- Lambda delivers to all users in each tenant
Direct Messages:
- Admin sends direct message to specific user
- SDK publishes single event targeting that user
Business Rules
- Priority mapping: Actions auto-assign priority (created=MEDIUM, status_changed=LOW, sla_breach=HIGH)
- Message truncation: Reply content is capped at 100 characters in notification body
- Sender exclusion: Actors are automatically excluded from receiving their own notifications
- Internal notes: Platform admin internal notes do not trigger notifications
- Graceful degradation: Notification failures are logged but never block the triggering operation
Input/Output Expectations
Input: Structured params objects with tenant context, entity IDs, and action metadata
Output: Events published to EventBridge with standardized NotificationEventDetail schema
Edge Cases Handled
- Missing event bus configuration → operations silently skip notification
- Empty recipient list for announcements → warning logged, no events sent
- Optional fields (message, newStatus, assigneeName) → appropriate defaults applied
Technical Details
Technology Stack
- Runtime: Node.js 24+
- Language: TypeScript (ES2022 target)
- AWS SDK: @aws-sdk/client-eventbridge ^3.696.0
- Build: TypeScript compiler (tsc)
- Testing: Jest with ts-jest
File Structure
notification-sdk/
├── src/
│ ├── index.ts # Public API exports and usage documentation
│ ├── client.ts # NotificationClient class implementation
│ └── types.ts # TypeScript type definitions for all events
├── package.json # Package metadata and scripts
├── tsconfig.json # TypeScript compiler configuration
└── jest.config.js # Test configurationKey Classes and Functions
NotificationClient (src/client.ts)
Main entry point. Wraps EventBridgeClient and provides typed methods for each notification type.
const client = new NotificationClient({
eventBusName: 'ketrics-notifications-dev',
source: 'ketrics.tenant-api', // or 'ketrics.platform'
region: 'us-east-1',
});Methods:
sendTicketNotification(params)- Ticket lifecycle eventssendAnnouncement(params)- Platform announcementssendDirectMessage(params)- User-to-user messagessendSlaBreachNotification(params)- SLA breach alertssendRawNotification(detailType, detail)- Advanced use cases
buildTicketNotificationContent() (src/client.ts:232-296)
Private method that maps ticket actions to notification content:
| Action | Priority | Type | Subject Template |
|--------|----------|------|------------------|
| created | MEDIUM | TICKET_CREATED | "New Support Ticket: {ticketRef}" |
| replied | MEDIUM | TICKET_REPLIED | "Reply on {ticketRef}" |
| status_changed | LOW | TICKET_STATUS_CHANGED | "Status Updated: {ticketRef}" |
| assigned | MEDIUM | TICKET_ASSIGNED | "Ticket Assigned: {ticketRef}" |
| sla_breach | HIGH | SLA_BREACH | "SLA Breach Warning: {ticketRef}" |
Type Definitions
Event Detail Types (src/types.ts)
interface NotificationEventDetail {
notificationId: string; // Unique ID for idempotency
tenantId: string; // Tenant context
timestamp: string; // ISO 8601
recipients: NotificationRecipients;
type: NotificationType;
priority: NotificationPriority;
subject: string;
body: string;
relatedEntity?: RelatedEntity;
sender: NotificationSender;
channels?: NotificationChannels;
}Recipient Types:
USER- Specific user IDsTENANT_ADMINS- All admins in a tenantALL_TENANT_USERS- Every user in a tenantPLATFORM_ADMINS- Platform-level administrators
Detail Types (EventBridge routing):
notification.ticket- Support ticket eventsnotification.announcement- Platform announcementsnotification.direct-message- Direct messages
Configuration
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| eventBusName | string | Yes | EventBridge event bus name |
| source | EventSource | Yes | ketrics.tenant-api or ketrics.platform |
| region | string | No | AWS region (uses SDK default if omitted) |
Environment Variables
The SDK itself has no environment variables. Configuration is passed via constructor. Consuming services typically use:
NOTIFICATION_EVENT_BUS_NAME=ketrics-notifications-dev
AWS_REGION=us-east-1External Integrations
- AWS EventBridge: Single integration point. Uses
PutEventsCommandto publish events.
Data Flow
Inbound
Data enters via typed method parameters from tenant-api or platform-api:
await client.sendTicketNotification({
tenantId: 'tenant-uuid',
ticketId: 'ticket-uuid',
ticketRef: 'TKT-0001',
action: 'replied',
actorId: 'user-uuid',
actorName: 'John Doe',
actorType: 'PLATFORM_ADMIN',
message: 'Here is my response...',
recipientUserIds: ['target-user-uuid'],
excludeUserIds: ['user-uuid'],
});Transformations
- Action mapping:
buildTicketNotificationContent()converts action to priority/type/subject/body - ID generation:
notificationIdcreated from entity ID + action + timestamp - Recipient resolution: recipientUserIds presence determines USER vs TENANT_ADMINS targeting
- Serialization: Detail object serialized to JSON for EventBridge
Outbound
EventBridge event structure:
{
"EventBusName": "ketrics-notifications-dev",
"Source": "ketrics.tenant-api",
"DetailType": "notification.ticket",
"Detail": "{\"notificationId\":\"ticket-123-replied-1706123456789\",...}"
}Announcement Fan-out
For announcements targeting multiple tenants, the SDK sends one event per tenant:
// Internally loops and sends:
for (const tenantId of params.targetTenantIds) {
await this.putEvent({
detailType: 'notification.announcement',
detail: { ...detail, tenantId, notificationId: `${baseId}-${tenantId}` }
});
}Error Handling
Error Scenarios
| Scenario | Behavior | |----------|----------| | EventBridge API error | Exception propagates to caller | | Network timeout | Exception propagates to caller | | Invalid parameters | TypeScript compile-time errors | | Missing event bus config | Caller should check config and not instantiate client |
Retry Logic
The SDK does not implement retries. Calling services implement their own retry strategies. Currently, both tenant-api and platform-api wrap calls in try/catch and log failures without retrying--notifications are considered best-effort.
Example from platform-api/src/services/ticket-notification-service.ts:85-91:
} catch (error) {
logger.error("Failed to send ticket reply notification", {
error: error instanceof Error ? error.message : "Unknown error",
ticketId: params.ticketId,
});
}Logging Approach
The SDK itself does not log. Calling services are responsible for logging:
- Success: Info-level logs with entity IDs
- Failure: Error-level logs with error details and context
Usage
Installation
npm install @ketrics/notification-sdkOr as a local dependency in the monorepo:
{
"dependencies": {
"@ketrics/notification-sdk": "file:../notification-sdk"
}
}Basic Usage
import { NotificationClient } from '@ketrics/notification-sdk';
const client = new NotificationClient({
eventBusName: 'ketrics-notifications-dev',
source: 'ketrics.tenant-api',
region: 'us-east-1',
});
// Ticket created notification
await client.sendTicketNotification({
tenantId: 'tenant-uuid',
ticketId: 'ticket-uuid',
ticketRef: 'TKT-0001',
action: 'created',
actorId: 'user-uuid',
actorName: 'John Doe',
actorType: 'TENANT_ADMIN',
message: 'I need help with...',
});
// Platform announcement
await client.sendAnnouncement({
title: 'System Maintenance',
body: 'Scheduled maintenance on Saturday 2-4 AM UTC.',
priority: 'MEDIUM',
targetTenantIds: ['tenant-1', 'tenant-2'], // or omit for all
senderId: 'admin-uuid',
senderName: 'Platform Team',
});
// Direct message
await client.sendDirectMessage({
tenantId: 'tenant-uuid',
recipientUserId: 'user-uuid',
subject: 'Account Update',
body: 'Your account has been upgraded.',
senderId: 'admin-uuid',
senderName: 'Support Team',
senderType: 'PLATFORM_ADMIN',
});
// SLA breach (high priority, all channels)
await client.sendSlaBreachNotification({
tenantId: 'tenant-uuid',
ticketId: 'ticket-uuid',
ticketRef: 'TKT-0001',
ticketSubject: 'Urgent: System Down',
hoursOverdue: 4.5,
});Singleton Pattern (Recommended)
Both tenant-api and platform-api use a singleton pattern with lazy initialization:
let notificationClient: NotificationClient | null = null;
function getNotificationClient(): NotificationClient | null {
if (!config.notifications?.eventBusName) {
return null; // Notifications disabled
}
if (!notificationClient) {
notificationClient = new NotificationClient({
eventBusName: config.notifications.eventBusName,
source: 'ketrics.tenant-api',
region: config.aws.region,
});
}
return notificationClient;
}Development Commands
npm install # Install dependencies
npm run build # Compile TypeScript to dist/
npm run test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
npm run lint # Run ESLint
npm run lint:fix # Auto-fix lint issues
npm run typecheck # Type-check without emittingTesting
Tests should mock the EventBridgeClient:
import { NotificationClient } from '@ketrics/notification-sdk';
import { EventBridgeClient } from '@aws-sdk/client-eventbridge';
jest.mock('@aws-sdk/client-eventbridge');
const mockSend = jest.fn().mockResolvedValue({});
(EventBridgeClient as jest.Mock).mockImplementation(() => ({
send: mockSend,
}));
describe('NotificationClient', () => {
it('sends ticket notification to EventBridge', async () => {
const client = new NotificationClient({
eventBusName: 'test-bus',
source: 'ketrics.tenant-api',
});
await client.sendTicketNotification({
tenantId: 'tenant-1',
ticketId: 'ticket-1',
ticketRef: 'TKT-0001',
action: 'created',
actorId: 'user-1',
actorName: 'Test User',
actorType: 'TENANT_ADMIN',
});
expect(mockSend).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
Entries: expect.arrayContaining([
expect.objectContaining({
EventBusName: 'test-bus',
Source: 'ketrics.tenant-api',
DetailType: 'notification.ticket',
}),
]),
}),
})
);
});
});