npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tjoc/notifications

v4.0.0

Published

Notification service for The Journey of Code platform

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/notifications

Node ≥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 NotificationService explicitly — the old export 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 notifications singleton removed — instantiate explicitly
  • defaultFrom is now required when resendApiKey is set — throws on construction if missing
  • productName config option added — injected as {{productName}} into every template automatically
  • @tjoc/payments dependency removed — local payment interfaces used instead
  • @types/handlebars removed
  • resend bumped to ^3.0.0
  • resume-upload-welcome template removed (wisecv-specific)
  • ENVNODE_ENV in environment checks

Development

pnpm build      # tsup → dist/
pnpm typecheck  # tsc --noEmit