@lighthings/notification-sdk
v1.0.4
Published
Standalone SDK for the notification service — frontend and backend in one package
Downloads
58
Readme
Notifications SDK
A TypeScript monorepo with two SDKs for the notification service.
| Package | Use case |
|---|---|
| @notifysdk/server | Backend services — send notifications |
| @notifysdk/client | Frontend apps — display, manage, receive real-time notifications |
| @notifysdk/types | Shared TypeScript types (auto-installed as a dependency) |
Installation
# In a backend service (Node.js)
npm install @notifysdk/server
# In your frontend app (React, Vue, etc.)
npm install @notifysdk/client@notifysdk/server — Sending notifications
Setup
import { NotificationServerClient } from '@notifysdk/server';
const notifier = new NotificationServerClient({
baseUrl: process.env.NOTIFICATION_SERVICE_URL!, // e.g. http://notification-service:4001
serviceSecret: process.env.SERVICE_SECRET!,
retries: 3, // optional, default 3
timeoutMs: 10_000, // optional, default 10s
});Send on any channel
const result = await notifier.send({
channels: ['email', 'in-app'],
priority: 'high',
recipient: {
userId: 'u_123',
email: '[email protected]',
},
title: 'Your order shipped',
body: 'Order #4521 is on its way and will arrive by Friday.',
actionUrl: 'https://app.example.com/orders/4521',
});
console.log(result.notificationId); // uuid
console.log(result.queued); // ['email', 'in-app']Convenience helpers
// Single-channel shortcuts
await notifier.sendInApp({ recipient, title, body });
await notifier.sendEmail({ recipient, title, body, email: { subject: 'Custom subject' } });
await notifier.sendSms({ recipient, title, body, sms: { body: 'Short SMS text' } });
await notifier.sendPush({ recipient, title, body });
await notifier.sendAll({ recipient, title, body }); // all 4 channelsIdempotency — safe retries
import { idempotencyKey } from '@notifysdk/server';
await notifier.send({
idempotencyKey: idempotencyKey('order-shipped', orderId, userId),
// ...rest of payload
});
// Calling this again with the same key returns the original result — no duplicate notification.Batch sending
import { batchSend } from '@notifysdk/server';
const { succeeded, failed } = await batchSend(
notifier,
users.map((u) => ({
channels: ['email'],
recipient: { userId: u.id, email: u.email },
title: 'New feature announcement',
body: '...',
})),
{ concurrency: 10 } // fire 10 requests at a time
);
console.log(`Sent: ${succeeded.length}, Failed: ${failed.length}`);
failed.forEach(({ payload, error }) => console.error(payload.recipient.userId, error.message));Email attachments
await notifier.sendEmail({
recipient: { userId: 'u_123', email: '[email protected]' },
title: 'Your invoice',
body: 'Please find your invoice attached.',
email: {
subject: 'Invoice #1042',
attachments: [{
filename: 'invoice-1042.pdf',
content: pdfBase64, // base64-encoded
contentType: 'application/pdf',
}],
},
});Options reference
| Option | Type | Default | Description |
|---|---|---|---|
| baseUrl | string | required | Notification service URL |
| serviceSecret | string | required | SERVICE_SECRET env var value |
| timeoutMs | number | 10000 | Per-request timeout |
| retries | number | 3 | Retry count on 5xx / network errors |
| retryDelayMs | number | 300 | Initial retry delay (doubles each attempt) |
@notifysdk/client — Frontend
Setup
import { NotificationClient } from '@notifysdk/client';
const client = new NotificationClient({
baseUrl: 'https://notifications.example.com',
userId: currentUser.id,
authToken: session.accessToken,
});Real-time notifications via SSE
// Register listeners before connecting
client
.onNotification((event) => {
const notif = event.data; // Notification object
showToast(notif.title);
unreadCount++;
})
.onRead((event) => {
// Another tab/device marked a notification read
markAsRead(event.data.notificationId);
})
.onReadAll(() => {
resetBadge();
});
// Open the SSE stream (auto-reconnects on drop)
client.connect();
// Close when the user logs out / component unmounts
client.disconnect();List & manage notifications
// Paginated list
const { notifications, total, unreadCount } = await client.list({ page: 1, limit: 20 });
// Unread only
const { notifications } = await client.list({ unreadOnly: true });
// Unread count badge
const count = await client.getUnreadCount();
// Mark one read
await client.markRead(notificationId);
// Mark all read
await client.markAllRead();
// Delete
await client.delete(notificationId);Token refresh
// After refreshing the session token:
client.setAuthToken(newAccessToken);
// SSE connection is unaffected (it uses userId in the URL, not the token)React integration example
import { useEffect, useState } from 'react';
import { NotificationClient, Notification } from '@notifysdk/client';
function useNotifications(userId: string, authToken: string) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
const client = new NotificationClient({
baseUrl: import.meta.env.VITE_NOTIFICATIONS_URL,
userId,
authToken,
});
// Load initial data
client.list().then(({ notifications, unreadCount }) => {
setNotifications(notifications);
setUnreadCount(unreadCount);
});
// Real-time updates
client
.onNotification((e) => {
setNotifications((prev) => [e.data, ...prev]);
setUnreadCount((c) => c + 1);
})
.onRead((e) => {
setNotifications((prev) =>
prev.map((n) => n._id === e.data.notificationId ? { ...n, read: true } : n)
);
setUnreadCount((c) => Math.max(0, c - 1));
})
.onReadAll(() => {
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
setUnreadCount(0);
})
.connect();
return () => client.disconnect();
}, [userId, authToken]);
return { notifications, unreadCount };
}Web Push subscription
// Register for push notifications (asks browser permission)
await client.push.subscribe(workspaceId?); // returns PushSubscription | null
// Check current subscription
const sub = await client.push.getSubscription();
// Unsubscribe
await client.push.unsubscribe();Your service worker should handle push events:
// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon ?? '/icon-192.png',
badge: data.badge ?? '/badge-72.png',
image: data.image,
tag: data.tag,
data: { actionUrl: data.actionUrl },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.actionUrl;
if (url) event.waitUntil(clients.openWindow(url));
});Advanced: use sub-modules directly
import { NotificationsApi, SSEManager, PushManager } from '@notifysdk/client';
// If you need fine-grained control, use these classes directly
// rather than the NotificationClient facade.
const api = new NotificationsApi({ baseUrl, authToken });
const sse = new SSEManager({ url: `${baseUrl}/sse/${userId}` });
const push = new PushManager(api, userId);Error handling
import { NotificationServiceError } from '@notifysdk/server';
import { NotificationClientError } from '@notifysdk/client';
try {
await notifier.send({ ... });
} catch (err) {
if (err instanceof NotificationServiceError) {
console.error(err.statusCode, err.message);
}
}
try {
await client.markRead(id);
} catch (err) {
if (err instanceof NotificationClientError) {
console.error(err.statusCode, err.message);
}
}Monorepo structure
notifications-sdk/
├── package.json # npm workspaces root
└── packages/
├── types/ # @notifysdk/types — shared interfaces
│ └── src/index.ts
├── server/ # @notifysdk/server — backend send SDK
│ └── src/
│ ├── client.ts # NotificationServerClient
│ ├── batch.ts # batchSend helper
│ └── index.ts
└── client/ # @notifysdk/client — frontend SDK
└── src/
├── client.ts # NotificationClient (facade)
├── api.ts # NotificationsApi (REST)
├── sse.ts # SSEManager (real-time)
├── push.ts # PushManager (Web Push)
└── index.tsBuilding
# Build all packages in dependency order
npm run build
# Watch mode during development
npm run dev