@tjoc/notifications
v4.0.0
Published
Notification service for The Journey of Code platform
Maintainers
Readme
@tjoc/notifications
Transactional email (Resend) and push notifications (Firebase FCM) for the tjoc.dev portfolio. Handlebars template engine with {{productName}} branding variable. Trigger system and automation rule engine included.
Installation
pnpm add @tjoc/notificationsNode ≥18. Published to npm (@tjoc/[email protected]).
Quick start
import { NotificationService } from '@tjoc/notifications';
const notifications = new NotificationService({
resendApiKey: process.env.RESEND_API_KEY!,
defaultFrom: '[email protected]', // required when resendApiKey is set
defaultReplyTo: '[email protected]', // optional
productName: 'Your App', // injected into every template as {{productName}}
firebaseCredentials: { // optional — omit if push not needed
projectId: process.env.FIREBASE_PROJECT_ID!,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
privateKey: process.env.FIREBASE_PRIVATE_KEY!,
},
});
// Send a direct email
await notifications.sendEmail({
to: '[email protected]',
subject: 'Welcome!',
html: '<h1>Welcome to Your App!</h1>',
});
// Send using a built-in template
await notifications.sendEmail({
to: '[email protected]',
templateId: 'email-activation-welcome',
templateData: {
name: 'Joe',
loginDate: new Date(),
dashboardUrl: 'https://app.example.com/dashboard',
},
});
// Send a push notification
await notifications.sendPushNotification('device-token', {
title: 'New message',
body: 'You have a new notification.',
});No singleton export. Instantiate
NotificationServiceexplicitly — the oldexport const notifications = new NotificationService(...)global was removed in v3.1.0.
Configuration
interface NotificationConfig {
resendApiKey?: string; // omit for push-only usage
defaultFrom?: string; // required when resendApiKey is set
defaultReplyTo?: string;
productName?: string; // injected as {{productName}} into every template
firebaseCredentials?: {
projectId: string;
clientEmail: string;
privateKey: string;
};
notificationRepository?: NotificationRepository; // optional in-app persistence
}Built-in templates
User action templates
| Template ID | Trigger | Key variables |
|---|---|---|
| email-activation-welcome | User verifies email | name, loginDate, dashboardUrl |
| new-device-login-alert | Login from new device | name, deviceInfo, location, loginTime, securityUrl |
| password-changed-confirmation | Password changed | name, changeTime, deviceInfo, securityUrl |
Payment templates
| Template ID | Trigger | Key variables |
|---|---|---|
| payment-success | Payment succeeded | name, amount, currency, date, receiptUrl |
| payment-failed | Payment failed | name, amount, currency, date, retryUrl |
| subscription-created | Sub started | name, planName, amount, currency, nextBillingDate |
| subscription-renewed | Sub renewed | name, planName, amount, currency, nextBillingDate |
| subscription-cancelled | Sub cancelled | name, planName, endDate, resubscribeUrl |
| subscription-expired | Sub expired | name, planName, resubscribeUrl |
| subscription-payment-failed | Sub payment failed | name, planName, amount, currency, retryUrl |
| refund-processed | Refund issued | name, amount, currency, date, supportUrl |
All templates automatically inject {{productName}} from the service-level productName config — no need to pass it per send.
Template management
import { TemplatesService } from '@tjoc/notifications';
const templates = new TemplatesService({ productName: 'Your App' });
templates.addTemplate({
id: 'custom-alert',
name: 'Custom Alert',
type: 'email',
content: {
subject: 'Alert from {{productName}}',
body: 'Hi {{name}}, something happened.',
html: '<p>Hi {{name}}, something happened on {{productName}}.</p>',
},
variables: [
{ name: 'name', type: 'string', required: true },
],
});In-app notification persistence
Provide a NotificationRepository to persist a copy of every email and push notification for in-app display:
interface NotificationRepository {
save(notification: Notification): Promise<void>;
findByUser(userId: string, options?: { limit?: number; offset?: number }): Promise<Notification[]>;
markAsRead(notificationId: string): Promise<void>;
findById?(notificationId: string): Promise<Notification | null>;
}const notifications = new NotificationService({
resendApiKey: '...',
defaultFrom: '[email protected]',
notificationRepository: myDbRepo,
});
// Fetch and mark read
const items = await notifications.getUserNotifications('user-123', { limit: 20 });
await notifications.markNotificationRead('notif-id');Trigger system and automation engine
import { NotificationTriggerService, NotificationAutomationEngine } from '@tjoc/notifications';
const triggerService = new NotificationTriggerService(notifications);
const automationEngine = new NotificationAutomationEngine(
notifications,
notifications.getTemplatesService(),
triggerService,
);
// Register a trigger
triggerService.registerTrigger({
id: 'user-signup',
name: 'User Signup Welcome',
eventType: 'EMAIL_ACTIVATION',
enabled: true,
templateId: 'email-activation-welcome',
notificationTypes: ['email'],
});
// Fire the trigger
await triggerService.fireEvent('EMAIL_ACTIVATION', {
userId: 'user-123',
user: { email: '[email protected]', name: 'Joe' },
});
// Add an automation rule with scheduling and frequency controls
await automationEngine.addRule({
id: 'welcome-sequence',
name: 'Welcome Email Sequence',
triggerId: 'user-signup',
actions: [{ type: 'send_notification', templateId: 'email-activation-welcome', notificationTypes: ['email'], delay: 0 }],
scheduling: { timezone: 'UTC', allowedHours: { start: 9, end: 17 }, allowedDays: [1, 2, 3, 4, 5] },
frequency: { maxPerDay: 1, cooldownMinutes: 60 },
enabled: true,
});Push notifications (FCM)
// Single device
await notifications.sendPushNotification('device-token', {
title: 'New message',
body: 'You have a new notification.',
imageUrl: 'https://example.com/img.jpg',
clickAction: 'https://example.com/messages',
});
// Multiple devices
await notifications.sendPushToDevices(['token1', 'token2'], { title: 'Update', body: 'v2.0 is live.' });
// Topic
await notifications.subscribeToTopic(['token1', 'token2'], 'news');
await notifications.sendToTopic('news', { title: 'Breaking', body: 'See latest update.' });v3.1.0 changes (from v3.0.0)
export const notificationssingleton removed — instantiate explicitlydefaultFromis now required whenresendApiKeyis set — throws on construction if missingproductNameconfig option added — injected as{{productName}}into every template automatically@tjoc/paymentsdependency removed — local payment interfaces used instead@types/handlebarsremovedresendbumped to^3.0.0resume-upload-welcometemplate removed (wisecv-specific)ENV→NODE_ENVin environment checks
Development
pnpm build # tsup → dist/
pnpm typecheck # tsc --noEmit