KS
Killer-Skills

revendiste-notification-system — how to use revendiste-notification-system how to use revendiste-notification-system, revendiste-notification-system setup guide, react email template examples, type-safe notification system, scalable notification architecture, revendiste-notification-system vs competitors

v1.0.0
GitHub

About this Skill

Perfect for Full Stack Agents needing scalable and type-safe notification solutions with email templating capabilities. revendiste-notification-system is a type-safe, scalable notification system with email template support using React Email.

Features

Utilizes React Email for customizable email templates
Provides type-safe notification functionality
Includes helper functions for common notification scenarios, such as notifySellerTicketSold
Supports scalable notification architecture
Employs TypeScript for robust and maintainable code

# Core Topics

mathfalcon mathfalcon
[0]
[0]
Updated: 3/7/2026

Quality Score

Top 5%
60
Excellent
Based on code quality & docs
Installation
SYS Universal Install (Auto-Detect)
Cursor IDE Windsurf IDE VS Code IDE
> npx killer-skills add mathfalcon/revendiste/revendiste-notification-system

Agent Capability Analysis

The revendiste-notification-system MCP Server by mathfalcon is an open-source Categories.community integration for Claude and other AI agents, enabling seamless task automation and capability expansion. Optimized for how to use revendiste-notification-system, revendiste-notification-system setup guide, react email template examples.

Ideal Agent Persona

Perfect for Full Stack Agents needing scalable and type-safe notification solutions with email templating capabilities.

Core Value

Empowers agents to send type-safe notifications using React Email for templating, providing a reliable and scalable notification system with features like notifySellerTicketSold and notifyOrderConfirmed functions.

Capabilities Granted for revendiste-notification-system MCP Server

Sending automated email notifications to users
Generating type-safe notification templates with React Email
Implementing scalable notification systems for e-commerce applications

! Prerequisites & Limits

  • Requires React Email for templating
  • TypeScript compatibility required
Project
SKILL.md
31.2 KB
.cursorrules
1.2 KB
package.json
240 B
Ready
UTF-8

# Tags

[No tags]
SKILL.md
Readonly

Revendiste Notification System

A type-safe, scalable notification system with email template support using React Email.

Quick Start

typescript
1import {NotificationService} from '~/services/notifications'; 2import { 3 notifySellerTicketSold, 4 notifyOrderConfirmed, 5 notifyIdentityVerificationCompleted, 6 notifyIdentityVerificationRejected, 7} from '~/services/notifications/helpers'; 8 9// Simple usage with helper function 10await notifySellerTicketSold(notificationService, { 11 sellerUserId: userId, 12 listingId: listing.id, 13 eventName: order.event.name, 14 eventStartDate: new Date(order.event.eventStartDate), 15 eventEndDate: new Date(order.event.eventEndDate), 16 platform: 'ticketmaster', 17 qrAvailabilityTiming: '12h', 18 ticketCount: 2, 19}); 20 21// Identity verification completed (user can now sell) 22await notifyIdentityVerificationCompleted(notificationService, { 23 userId: user.id, 24}); 25 26// Identity verification rejected by admin 27await notifyIdentityVerificationRejected(notificationService, { 28 userId: user.id, 29 rejectionReason: 'El documento no es legible', 30 canRetry: true, 31}); 32 33// Or create directly (title and description are auto-generated from metadata) 34await notificationService.createNotification({ 35 userId: userId, 36 type: 'order_confirmed', 37 channels: ['in_app', 'email'], 38 actions: [ 39 { 40 type: 'view_order', 41 label: 'Ver orden', 42 url: `${APP_BASE_URL}/cuenta/tickets?orderId=${orderId}`, 43 }, 44 ], 45 metadata: { 46 type: 'order_confirmed', 47 orderId, 48 eventName, 49 totalAmount: '100.00', 50 subtotalAmount: '90.00', 51 platformCommission: '10.00', 52 vatOnCommission: '0.00', 53 currency: 'EUR', 54 items: [], 55 }, 56});

Note: Title and description are automatically generated from the notification type and metadata - you don't need to provide them when creating notifications.

Debounced/Batched Notifications

Some notifications benefit from batching to avoid spam. For example, when a seller uploads multiple ticket documents for the same order, we don't want to send separate emails for each upload. Instead, we batch them into a single notification.

How It Works

  1. Debounce Key: Notifications are grouped by a unique key (e.g., document_uploaded:{orderId})
  2. Time Window: When the first notification is added to a batch, a time window starts (default: 5 minutes)
  3. Batching: Additional notifications with the same key are added to the batch
  4. Processing: When the window ends, a cronjob merges all items into a single notification
  5. Final Notification: The merged notification is sent (e.g., "3 de tus 4 entradas están listas")

Using Debounced Notifications

typescript
1// notifyDocumentUploaded automatically uses debouncing 2await notifyDocumentUploaded(notificationService, { 3 buyerUserId: buyer.id, 4 orderId: order.id, 5 eventName: 'Concert', 6 ticketCount: 1, 7}); 8 9// Or create a debounced notification directly 10await notificationService.createDebouncedNotification({ 11 userId: buyer.id, 12 type: 'document_uploaded', 13 channels: ['in_app', 'email'], 14 metadata: { ... }, 15 actions: [ ... ], 16 debounce: { 17 key: `document_uploaded:${orderId}`, // Unique grouping key 18 windowMs: 5 * 60 * 1000, // 5 minute window 19 }, 20});

Database Tables

  • notification_batches: Groups related notifications by debounce key
  • notification_batch_items: Individual items within a batch (metadata, actions)

Cronjob Processing

The process-pending-notifications cronjob handles both:

  1. Pending batches: Merges items when window ends, creates final notification
  2. Pending notifications: Retries failed sends with exponential backoff

Notification Types with Debouncing

  • document_uploaded → merged into document_uploaded_batch

Available Helper Functions

Helper functions simplify notification creation with pre-configured channels, actions, and metadata structure:

Order & Ticket Helpers:

  • notifySellerTicketSold() - Notify seller when tickets sold
  • notifyDocumentReminder() - Remind seller to upload documents
  • notifyDocumentUploaded() - Notify buyer when documents uploaded (uses debouncing)
  • notifyDocumentUploadedImmediate() - Same as above but immediate (no debouncing)
  • notifyOrderConfirmed() - Order confirmation
  • notifyOrderExpired() - Order expiration
  • notifyPaymentFailed() - Payment failure

Payout Helpers:

  • notifyPayoutCompleted() - Payout completed
  • notifyPayoutFailed() - Payout failed
  • notifyPayoutCancelled() - Payout cancelled

Identity Verification Helpers:

  • notifyIdentityVerificationCompleted() - Verification successful
  • notifyIdentityVerificationRejected() - Admin rejected verification
  • notifyIdentityVerificationFailed() - System failure (in_app only)
  • notifyIdentityVerificationManualReview() - Needs admin review (in_app only)

System Architecture

Type-Safe Notification System

The notification system uses discriminated unions and function overloading to provide full type safety:

  1. Metadata Schemas (packages/shared/src/schemas/notifications.ts)

    • All notification schemas are in the shared package
    • Each notification type has its own Zod schema
    • Discriminated union ensures type safety
    • Metadata type must match notification type
    • Includes base schemas, action schemas, and notification schemas
  2. Email Templates (packages/transactional/)

    • React Email components in emails/ directory
    • Each template exports its prop types
    • Type-safe template mapping via function overloading
  3. Template Builder (apps/backend/src/services/notifications/email-template-builder.ts)

    • Parses metadata using correct schema from shared package
    • Maps notification types to email templates
    • Renders React components to HTML (no React in backend)
  4. Database Types (packages/shared/src/types/db.d.ts)

    • Generated database types from Kysely
    • NotificationType enum is generated from database
    • Notification model type is in apps/backend/src/types/models.ts as Selectable<Notifications>

Notification Types

Current Types

Order & Ticket Notifications:

  • ticket_sold_seller - Seller notification when tickets are sold
  • document_reminder - Seller reminder to upload documents
  • document_uploaded - Buyer notification when seller uploads ticket documents
  • order_confirmed - Order confirmation
  • order_expired - Order expiration

Payment Notifications:

  • payment_failed - Payment failure
  • payment_succeeded - Payment success

Payout Notifications:

  • payout_processing - Payout started processing (legacy, maps to completed)
  • payout_completed - Payout completed successfully
  • payout_failed - Payout failed
  • payout_cancelled - Payout cancelled

Identity Verification Notifications:

  • identity_verification_completed - Verification successful (auto or admin approved)
  • identity_verification_rejected - Admin rejected verification
  • identity_verification_failed - System failure (face mismatch, liveness fail)
  • identity_verification_manual_review - Borderline scores, needs admin review

Auth Notifications (Clerk webhook triggered):

  • auth_verification_code - OTP for email verification
  • auth_reset_password_code - OTP for password reset
  • auth_invitation - User invitation
  • auth_password_changed - Password changed notification
  • auth_password_removed - Password removed notification
  • auth_primary_email_changed - Primary email changed notification
  • auth_new_device_sign_in - New device sign-in alert

Notification Action Types

Actions allow in-app notifications to be clickable and redirect users:

  • upload_documents - Redirect to upload ticket documents
  • view_order - Redirect to view order details
  • retry_payment - Redirect to retry payment
  • view_payout - Redirect to view payout details
  • start_verification - Redirect to start/retry identity verification
  • publish_tickets - Redirect to publish tickets page

Notification Channels

  • in_app - In-app notifications (bell icon)
  • email - Email notifications
  • sms - SMS notifications (future)

Channel Selection Strategy

Use ['in_app', 'email'] for high-value notifications:

  • Order confirmations (order_confirmed)
  • Payment failures (payment_failed)
  • Payout completions (payout_completed)
  • Identity verification completed (identity_verification_completed)
  • Identity verification rejected (identity_verification_rejected)
  • Document uploads (document_uploaded)

Use ['in_app'] only to save email costs:

  • Informational updates that don't require immediate action
  • Manual review status (identity_verification_manual_review)
  • System failures where user can retry in UI (identity_verification_failed)
  • Transient states
typescript
1// Example: in_app only (no email cost) 2await service.createNotification({ 3 userId, 4 type: 'identity_verification_manual_review', 5 channels: ['in_app'], // No email - just informational 6 actions: null, 7 metadata: {type: 'identity_verification_manual_review'}, 8}); 9 10// Example: high-value notification (email + in_app) 11await service.createNotification({ 12 userId, 13 type: 'identity_verification_completed', 14 channels: ['in_app', 'email'], // Email is valuable here 15 actions: [{type: 'publish_tickets', label: 'Publicar entradas', url: '...'}], 16 metadata: {type: 'identity_verification_completed'}, 17});

Notification Status

  • pending - Created but not yet sent
  • sent - Successfully sent (all channels succeeded or partial success)
  • failed - Failed to send (all channels failed, will be retried by cronjob)
  • seen - User has seen the notification (in-app only)

Channel-Level Tracking

Each notification tracks delivery status per channel:

  • channelStatus (JSONB): Tracks status for each channel individually
    • Format: {"email": {"status": "sent", "sentAt": "..."}, "in_app": {"status": "failed", "error": "..."}}
    • Allows partial success (e.g., email sent but SMS failed)
    • Overall notification status is sent if any channel succeeds

Retry Mechanism

  • retryCount (integer): Tracks number of retry attempts (max 5)
  • Exponential backoff: Wait time increases with each retry
    • Retry 0: 5 minutes
    • Retry 1: 10 minutes
    • Retry 2: 20 minutes
    • Retry 3: 40 minutes
    • Retry 4: 80 minutes
  • Cron job processes pending notifications every 5 minutes
  • Processes in parallel batches (10 at a time) for better throughput

Adding a New Notification Type

To add a new notification type, follow these steps:

Step 1: Define Metadata Schema

In packages/shared/src/schemas/notifications.ts:

typescript
1// 1. Add metadata schema 2export const MyNewNotificationMetadataSchema = z.object({ 3 type: z.literal('my_new_notification'), 4 // Add your fields here 5 orderId: z.uuid(), 6 eventName: z.string(), 7 customField: z.string(), 8}); 9 10// 2. Add action schema (if needed) 11// First, add your new action type to NotificationActionType enum if needed: 12export const NotificationActionType = z.enum([ 13 'upload_documents', 14 'view_order', 15 'retry_payment', 16 'view_payout', 17 'start_verification', 18 'publish_tickets', 19 'my_new_action', // Add your new action type here 20]); 21 22export const MyNewNotificationActionsSchema = z 23 .array( 24 BaseActionSchema.extend({ 25 type: z.literal('my_new_action'), // Use your specific action type 26 label: z.string(), 27 url: z.url(), // Use z.url() not z.string().url() 28 }), 29 ) 30 .nullable(); 31 32// 3. Add notification schema 33export const MyNewNotificationSchema = BaseNotificationSchema.extend({ 34 type: z.literal('my_new_notification'), 35 metadata: MyNewNotificationMetadataSchema, 36 actions: MyNewNotificationActionsSchema, 37}); 38 39// 4. Add to discriminated unions 40export const NotificationMetadataSchema = z.discriminatedUnion('type', [ 41 // ... existing schemas 42 MyNewNotificationMetadataSchema, // Add here 43]); 44 45export const NotificationSchema = z.discriminatedUnion('type', [ 46 // ... existing schemas 47 MyNewNotificationSchema, // Add here 48]);

Step 2: Create Email Template Component

In packages/transactional/emails/my-new-notification-email.tsx:

typescript
1import {Button, Section, Text} from '@react-email/components'; 2import {BaseEmail} from './base-template'; 3 4export interface MyNewNotificationEmailProps { 5 eventName: string; 6 orderId: string; 7 customField: string; 8 appBaseUrl?: string; 9} 10 11export const MyNewNotificationEmail = ({ 12 eventName, 13 orderId, 14 customField, 15 appBaseUrl, 16}: MyNewNotificationEmailProps) => ( 17 <BaseEmail 18 title="My New Notification" 19 preview={`Notification for ${eventName}`} 20 appBaseUrl={appBaseUrl} 21 > 22 <Text className="text-foreground mb-4">Content here...</Text> 23 </BaseEmail> 24); 25 26MyNewNotificationEmail.PreviewProps = { 27 eventName: 'Example Event', 28 orderId: '123', 29 customField: 'example', 30} as MyNewNotificationEmailProps; 31 32export default MyNewNotificationEmail;

Step 3: Export Template and Props

In packages/transactional/src/index.ts:

typescript
1// Export the component 2export * from '../emails/my-new-notification-email'; 3 4// Export prop types 5export type {MyNewNotificationEmailProps} from '../emails/my-new-notification-email';

Step 4: Add to Email Template Mapping

In packages/transactional/src/email-templates.ts:

typescript
1// 1. Import the component and props 2import { 3 MyNewNotificationEmail as MyNewNotificationEmailComponent, 4 type MyNewNotificationEmailProps, 5} from '../emails/my-new-notification-email'; 6import type {NotificationType, TypedNotificationMetadata} from '@revendiste/shared'; 7 8// Note: NotificationType is now imported from @revendiste/shared (generated from database) 9 10// 2. Add switch case in implementation 11case 'my_new_notification': { 12 const meta = metadata as TypedNotificationMetadata<'my_new_notification'>; 13 return { 14 Component: MyNewNotificationEmailComponent, 15 props: { 16 eventName: meta?.eventName || 'el evento', 17 orderId: meta?.orderId || '', 18 customField: meta?.customField || '', 19 appBaseUrl, 20 }, 21 }; 22}

Note: NotificationType is now generated from the database enum, so you don't need to manually add it to a union type. The database enum will be updated in Step 6.

Step 5: Add to Email Template Builder

Note: The email template builder now uses a unified getEmailTemplate() function from the transactional package. No changes needed here - the switch statement in packages/transactional/src/email-templates.ts handles all notification types.

Step 6: Update Database Enum (Migration)

Create a migration to add the new enum value using PostgreSQL's ALTER TYPE ... ADD VALUE:

typescript
1// In migration file 2export async function up(db: Kysely<any>): Promise<void> { 3 // Add new value to the enum (appends to the end by default) 4 await sql` 5 ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'my_new_notification' 6 `.execute(db); 7}

Options for enum value positioning:

sql
1-- Add at the end (default) 2ALTER TYPE notification_type ADD VALUE 'my_new_notification'; 3 4-- Add before an existing value 5ALTER TYPE notification_type ADD VALUE 'my_new_notification' BEFORE 'order_confirmed'; 6 7-- Add after an existing value 8ALTER TYPE notification_type ADD VALUE 'my_new_notification' AFTER 'payment_succeeded'; 9 10-- Use IF NOT EXISTS to avoid errors if value already exists 11ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'my_new_notification';

Important notes:

  • ALTER TYPE ... ADD VALUE cannot be executed inside a transaction block in PostgreSQL < 12
  • In PostgreSQL 12+, it can run inside a transaction but the new value cannot be used until after the transaction commits
  • For Kysely migrations, this is usually fine since each migration runs in its own transaction
  • See PostgreSQL ALTER TYPE documentation for more details

After running the migration, regenerate database types:

bash
1cd apps/backend && pnpm generate:db

This updates NotificationType in packages/shared/src/types/db.d.ts.

Step 7: Update Repository Interface

Note: After regenerating database types (Step 6), NotificationType will automatically include the new value. However, you may need to update the repository interface if it uses a union type instead of importing from shared:

In apps/backend/src/repositories/notifications/index.ts:

typescript
1import type {NotificationType} from '~/shared'; 2 3export interface CreateNotificationData { 4 // ... 5 type: NotificationType; // Uses generated enum type 6 // ... 7}

If the interface uses a union type, update it to include the new value, or better yet, import NotificationType from ~/shared.

Step 8: Add Text Generation Function

In packages/shared/src/utils/notification-text.ts, add a case to generateNotificationText():

typescript
1case 'my_new_notification': { 2 const meta = metadata as TypedNotificationMetadata<'my_new_notification'>; 3 return { 4 title: 'My New Notification', 5 description: `Notification for ${meta.eventName}`, 6 }; 7}

Note: Title and description are generated from metadata, not stored in the database. This function is called automatically when notifications are created or retrieved.

Step 9: (Optional) Create Helper Function

In apps/backend/src/services/notifications/helpers.ts:

typescript
1export async function notifyMyNewNotification( 2 service: NotificationService, 3 params: { 4 userId: string; 5 orderId: string; 6 eventName: string; 7 customField: string; 8 }, 9) { 10 return await service.createNotification({ 11 userId: params.userId, 12 type: 'my_new_notification', 13 channels: ['in_app', 'email'], 14 actions: [ 15 { 16 type: 'view_order', 17 label: 'View Details', 18 url: `${APP_BASE_URL}/orders/${params.orderId}`, 19 }, 20 ], 21 metadata: { 22 type: 'my_new_notification' as const, 23 orderId: params.orderId, 24 eventName: params.eventName, 25 customField: params.customField, 26 }, 27 }); 28}

Note: Helper functions don't need to provide title or description - they're automatically generated from the metadata.

Type System Architecture

Shared Package Organization

All notification schemas are in the shared package (packages/shared/src/schemas/notifications.ts):

  • Base schemas (BaseNotificationSchema, BaseActionSchema)
  • Metadata schemas for each notification type
  • Action schemas for each notification type
  • Notification schemas for each notification type
  • Discriminated unions (NotificationMetadataSchema, NotificationSchema)
  • All related TypeScript types

Benefits:

  • Single source of truth for notification schemas
  • Shared between backend and transactional packages
  • Type-safe across the monorepo
  • Easy to maintain and extend

Discriminated Union Pattern

The notification system uses discriminated unions for type safety:

typescript
1// Each notification type has its own metadata schema (in shared package) 2export const TicketSoldSellerMetadataSchema = z.object({ 3 type: z.literal('ticket_sold_seller'), // Discriminator 4 listingId: z.uuid(), 5 eventName: z.string(), 6 eventStartDate: z.string(), 7 ticketCount: z.number().int().positive(), 8 platform: z.string(), 9 qrAvailabilityTiming: z.custom<QrAvailabilityTiming>().nullable().optional(), 10 shouldPromptUpload: z.boolean(), 11}); 12 13// Discriminated union ensures type safety 14export const NotificationMetadataSchema = z.discriminatedUnion('type', [ 15 TicketSoldSellerMetadataSchema, 16 DocumentReminderMetadataSchema, 17 DocumentUploadedMetadataSchema, 18 OrderConfirmedMetadataSchema, 19 // ... other schemas 20]); 21 22// TypeScript infers the correct type based on 'type' field 23type Metadata = z.infer<typeof NotificationMetadataSchema>; 24// Metadata is: TicketSoldSellerMetadata | DocumentReminderMetadata | ...

Database Type Integration

  • NotificationType is generated from the database enum in packages/shared/src/types/db.d.ts
  • Notification model type is in apps/backend/src/types/models.ts as Selectable<Notifications>
  • Always run pnpm generate:db after migrations to update types

Email Template Function

The getEmailTemplate() function maps notification types to their email templates:

typescript
1// Single function signature (no overloading needed) 2export function getEmailTemplate<T extends NotificationType>( 3 props: EmailTemplateProps<T>, 4): { 5 Component: React.ComponentType<any>; 6 props: Record<string, any>; 7}; 8 9// Switch statement handles type mapping 10switch (notificationType) { 11 case 'ticket_sold_seller': { 12 const meta = metadata as TypedNotificationMetadata<'ticket_sold_seller'>; 13 return { 14 Component: SellerTicketSoldEmailComponent, 15 props: { 16 eventName: meta?.eventName || 'el evento', 17 eventStartDate: meta?.eventStartDate || new Date().toISOString(), 18 ticketCount: meta?.ticketCount || 1, 19 uploadUrl: meta?.shouldPromptUpload ? uploadUrl : undefined, 20 appBaseUrl, 21 }, 22 }; 23 } 24 // ... other cases 25}

Note: Function overloading was simplified to a single signature with a switch statement, which is sufficient for runtime type mapping.

Title and Description Generation

Title and description are automatically generated from metadata - they are not stored in the database:

  • generateNotificationText() function in packages/shared/src/utils/notification-text.ts
  • Called automatically when notifications are created (for validation) and retrieved (for API responses)
  • Each notification type has its own text generation logic based on metadata
  • Ensures consistency and eliminates the need to store redundant text data

Notification Validation Flow

When creating a notification, the system validates data in this order:

  1. Metadata Validation (NotificationService.createNotification)

    • Validates metadata against NotificationMetadataSchema (discriminated union)
    • Ensures metadata type field matches notification type
    • Throws ValidationError if validation fails
  2. Actions Validation

    • Validates actions against NotificationActionsSchema (generic array schema)
    • Converts null to undefined for consistency
  3. Title/Description Generation

    • Generates title and description from metadata using generateNotificationText()
    • Metadata is required for this step (throws error if missing)
  4. Full Notification Validation

    • Validates complete notification structure against type-specific schema from NotificationSchema (discriminated union)
    • Ensures all fields match the expected structure for the notification type
  5. Database Creation

    • Creates notification record with validated data
    • Repository assumes data is already validated (no additional validation)

Email Template Builder Flow

  1. Parse Metadata (parseNotificationMetadata)

    • Validates metadata against correct schema
    • Returns typed metadata matching notification type
    • Throws error if metadata type doesn't match notification type
  2. Build Template (buildEmailTemplate)

    • Maps notification type to email template via getEmailTemplate()
    • Renders React component to HTML in transactional package using renderEmail()
    • Generates both HTML and plain text versions
    • Returns HTML and plain text ready to send
  3. Send Email (NotificationService.sendEmailNotification)

    • Calls template builder
    • Generates email subject from notification title (via generateNotificationText())
    • Sends via email provider (Resend, Console, etc.)

Email Provider Configuration

Environment Variables

env
1EMAIL_PROVIDER=resend # Options: console, resend 2EMAIL_FROM=noreply@yourdomain.com 3RESEND_API_KEY=re_your_api_key_here # Required when EMAIL_PROVIDER=resend

Provider Pattern

The system uses a factory pattern to select email providers:

  • Console Provider (default): Logs emails to console for development
  • Resend Provider: Production-ready email service with excellent deliverability

Switching Providers

Change EMAIL_PROVIDER environment variable - no code changes needed.

API Endpoints

  • GET /notifications - Get user notifications (with pagination)
  • GET /notifications/unseen-count - Get count of unseen notifications
  • PATCH /notifications/:id/seen - Mark notification as seen
  • PATCH /notifications/seen-all - Mark all as seen
  • DELETE /notifications/:id - Delete notification

Key Patterns

Title and Description Auto-Generation

Title and description are automatically generated from metadata - you never provide them:

typescript
1// ✅ CORRECT - Title/description auto-generated 2await notificationService.createNotification({ 3 userId: userId, 4 type: 'order_confirmed', 5 channels: ['in_app', 'email'], 6 metadata: { 7 type: 'order_confirmed', 8 orderId, 9 eventName, 10 // ... other fields 11 }, 12}); 13// Title: "Orden confirmada" 14// Description: Generated from metadata 15 16// ❌ WRONG - Don't provide title/description 17await notificationService.createNotification({ 18 userId: userId, 19 type: 'order_confirmed', 20 title: 'Custom title', // Not accepted! 21 description: 'Custom description', // Not accepted! 22 // ... 23});

Fire-and-Forget Processing

Notifications are sent asynchronously - your business logic doesn't wait:

typescript
1// ✅ CORRECT - Non-blocking 2await notificationService.createNotification({...}); 3// Your code continues immediately 4 5// The notification is sent in the background

External API Calls Outside Transactions

Email sending happens outside database transactions (follows Transaction Safety pattern):

typescript
1// ✅ CORRECT - Email outside transaction 2await this.repository.executeTransaction(async trx => { 3 // Database operations only 4 await repo.create({...}); 5}); 6 7// Email sent after transaction commits 8await notificationService.createNotification({...});

Error Handling

  • Failed notifications are marked with status failed and error message
  • Channel-level tracking: Each channel's status is tracked individually in channelStatus JSONB field
  • Exponential backoff: Retries wait longer with each attempt (5min, 10min, 20min, 40min, 80min)
    • Calculated as: baseDelay * 2^retryCount where baseDelay = 5 minutes
    • Filtered in application code after querying from database
  • Max 5 retries: Prevents infinite retry loops (retry count stored in retryCount column)
  • Cronjob automatically retries pending notifications in parallel batches (10 at a time)
  • Errors are logged but don't break your business logic
  • Partial success: If some channels succeed, notification is marked as sent
  • Notifications are skipped if already processed (status sent or failed, or all channels already processed)

Template System Details

React Email Components

Email templates are React components in packages/transactional/emails/:

  • Use @react-email/components for email-safe components
  • Export prop types for type safety
  • Use BaseEmail wrapper for consistent layout
  • Include PreviewProps for React Email preview

Template Mapping

Templates are mapped via getEmailTemplate() in packages/transactional/src/email-templates.ts:

  • Function overloading provides type safety
  • Each notification type maps to its specific component
  • Props are extracted from metadata and actions
  • TypeScript ensures correct props for each template

Rendering Flow

  1. Backend calls buildEmailTemplate() with typed metadata
  2. Template builder calls getEmailTemplate() (type-safe via overloading)
  3. React component is rendered to HTML using renderEmail() in transactional package
  4. HTML is sent via email provider

Key Point: React stays in the transactional package - backend never imports React directly.

Best Practices

  1. Use helper functions for common notification types - they handle metadata structure correctly
  2. Keep notifications outside transactions - send after database operations complete
  3. Don't await notification creation - let it process in background (fire-and-forget)
  4. Use appropriate channels - email for important actions, in-app for updates (see Channel Selection Strategy)
  5. Include actions conditionally - some notifications may not need actions (e.g., notifySellerTicketSold only adds upload action if within time window)
  6. Add metadata - store relevant IDs/context for future reference (required for title/description generation)
  7. Never provide title/description - they're auto-generated from metadata
  8. Export prop types - enables type safety in template mapping
  9. Follow discriminated union pattern - ensures metadata matches notification type
  10. Handle errors gracefully - notification failures are logged but don't break business logic
  11. Be conservative with emails - emails cost money; use ['in_app'] only for informational updates
  12. Add action types to enum - when creating notifications with actions, ensure action type is in NotificationActionType

Notifications Without Email Templates

Some notifications only use in_app channel and don't need email templates:

typescript
1// identity_verification_manual_review - in_app only 2await notifyIdentityVerificationManualReview(service, {userId}); 3 4// identity_verification_failed - in_app only (user can retry in UI) 5await notifyIdentityVerificationFailed(service, { 6 userId, 7 failureReason: 'No pudimos verificar tu identidad', 8 attemptsRemaining: 3, 9});

For these notifications:

  • Skip Step 2 (email template creation)
  • Skip Step 3 (template export)
  • Skip Step 4 (email template mapping)
  • Still need: metadata schema, action schema, notification schema, text generation, database enum, helper function

Maintenance Checklist

When adding a new notification type, ensure:

  • Metadata schema defined in packages/shared/src/schemas/notifications.ts
  • Action type added to NotificationActionType enum (if using new action type)
  • Action schema added (if needed) in packages/shared/src/schemas/notifications.ts
  • Notification schema added to discriminated union in packages/shared/src/schemas/notifications.ts
  • Text generation function added in packages/shared/src/utils/notification-text.ts (for title/description)
  • Email template component created with exported props in packages/transactional/emails/ (if using email channel)
  • Template exported from packages/transactional/src/index.ts
  • Switch case added in packages/transactional/src/email-templates.ts implementation
  • Database enum updated (migration created and run)
  • Types regenerated: cd apps/backend && pnpm generate:db (after migration)
  • Repository interface updated (if using union type instead of NotificationType from shared)
  • Helper function created in apps/backend/src/services/notifications/helpers.ts (optional but recommended)
  • API docs regenerated: pnpm tsoa:both

Quick Commands

bash
1# After creating migration for new notification type 2cd apps/backend && pnpm kysely:migrate && pnpm generate:db 3 4# Regenerate TSOA routes and OpenAPI spec 5cd apps/backend && pnpm tsoa:both 6 7# Regenerate frontend API types 8cd apps/frontend && pnpm generate:api

Type Safety Benefits

The type system ensures:

  • Compile-time validation - TypeScript catches type mismatches
  • Autocomplete support - IDE knows what props each template needs
  • Refactoring safety - Changes propagate through type system
  • Self-documenting - Types show exactly what each notification needs
  • No runtime checks needed - Type system handles validation

Related Skills

Looking for an alternative to revendiste-notification-system or building a Categories.community AI Agent? Explore these related open-source MCP Servers.

View All

widget-generator

Logo of f
f

widget-generator is an open-source AI agent skill for creating widget plugins that are injected into prompt feeds on prompts.chat. It supports two rendering modes: standard prompt widgets using default PromptCard styling and custom render widgets built as full React components.

149.6k
0
Design

chat-sdk

Logo of lobehub
lobehub

chat-sdk is a unified TypeScript SDK for building chat bots across multiple platforms, providing a single interface for deploying bot logic.

73.0k
0
Communication

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication