Agent Capability Analysis
The revendiste-notification-system skill by mathfalcon is an open-source community AI agent skill for Claude Code and other IDE workflows, helping agents execute tasks with better context, repeatability, and domain-specific guidance.
Ideal Agent Persona
Ideal for Notification Agents requiring scalable and type-safe email notification systems with React Email support.
Core Value
Empowers agents to send targeted notifications using email templates, leveraging type safety and scalability, with features like notifySellerTicketSold and notifyOrderConfirmed, utilizing React Email for seamless integration.
↓ Capabilities Granted for revendiste-notification-system
! Prerequisites & Limits
- Requires TypeScript compatibility
- Dependent on React Email for template rendering
- Limited to notification services defined in the ~/services/notifications module
Browser Sandbox Environment
⚡️ Ready to unleash?
Experience this Agent in a zero-setup browser environment powered by WebContainers. No installation required.
revendiste-notification-system
Install revendiste-notification-system, an AI agent skill for AI agent workflows and automation. Works with Claude Code, Cursor, and Windsurf with one-command...
Revendiste Notification System
A type-safe, scalable notification system with email template support using React Email.
Quick Start
typescript1import {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
- Debounce Key: Notifications are grouped by a unique key (e.g.,
document_uploaded:{orderId}) - Time Window: When the first notification is added to a batch, a time window starts (default: 5 minutes)
- Batching: Additional notifications with the same key are added to the batch
- Processing: When the window ends, a cronjob merges all items into a single notification
- Final Notification: The merged notification is sent (e.g., "3 de tus 4 entradas están listas")
Using Debounced Notifications
typescript1// 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:
- Pending batches: Merges items when window ends, creates final notification
- Pending notifications: Retries failed sends with exponential backoff
Notification Types with Debouncing
document_uploaded→ merged intodocument_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 soldnotifyDocumentReminder()- Remind seller to upload documentsnotifyDocumentUploaded()- Notify buyer when documents uploaded (uses debouncing)notifyDocumentUploadedImmediate()- Same as above but immediate (no debouncing)notifyOrderConfirmed()- Order confirmationnotifyOrderExpired()- Order expirationnotifyPaymentFailed()- Payment failure
Payout Helpers:
notifyPayoutCompleted()- Payout completednotifyPayoutFailed()- Payout failednotifyPayoutCancelled()- Payout cancelled
Identity Verification Helpers:
notifyIdentityVerificationCompleted()- Verification successfulnotifyIdentityVerificationRejected()- Admin rejected verificationnotifyIdentityVerificationFailed()- 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:
-
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
-
Email Templates (
packages/transactional/)- React Email components in
emails/directory - Each template exports its prop types
- Type-safe template mapping via function overloading
- React Email components in
-
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)
-
Database Types (
packages/shared/src/types/db.d.ts)- Generated database types from Kysely
NotificationTypeenum is generated from databaseNotificationmodel type is inapps/backend/src/types/models.tsasSelectable<Notifications>
Notification Types
Current Types
Order & Ticket Notifications:
ticket_sold_seller- Seller notification when tickets are solddocument_reminder- Seller reminder to upload documentsdocument_uploaded- Buyer notification when seller uploads ticket documentsorder_confirmed- Order confirmationorder_expired- Order expiration
Payment Notifications:
payment_failed- Payment failurepayment_succeeded- Payment success
Payout Notifications:
payout_processing- Payout started processing (legacy, maps to completed)payout_completed- Payout completed successfullypayout_failed- Payout failedpayout_cancelled- Payout cancelled
Identity Verification Notifications:
identity_verification_completed- Verification successful (auto or admin approved)identity_verification_rejected- Admin rejected verificationidentity_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 verificationauth_reset_password_code- OTP for password resetauth_invitation- User invitationauth_password_changed- Password changed notificationauth_password_removed- Password removed notificationauth_primary_email_changed- Primary email changed notificationauth_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 documentsview_order- Redirect to view order detailsretry_payment- Redirect to retry paymentview_payout- Redirect to view payout detailsstart_verification- Redirect to start/retry identity verificationpublish_tickets- Redirect to publish tickets page
Notification Channels
in_app- In-app notifications (bell icon)email- Email notificationssms- 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
typescript1// 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 sentsent- 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
sentif any channel succeeds
- Format:
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:
typescript1// 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:
typescript1import {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:
typescript1// 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:
typescript1// 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:
typescript1// 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:
sql1-- 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 VALUEcannot 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:
bash1cd 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:
typescript1import 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():
typescript1case '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:
typescript1export 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:
typescript1// 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
NotificationTypeis generated from the database enum inpackages/shared/src/types/db.d.tsNotificationmodel type is inapps/backend/src/types/models.tsasSelectable<Notifications>- Always run
pnpm generate:dbafter migrations to update types
Email Template Function
The getEmailTemplate() function maps notification types to their email templates:
typescript1// 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 inpackages/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:
-
Metadata Validation (
NotificationService.createNotification)- Validates metadata against
NotificationMetadataSchema(discriminated union) - Ensures metadata
typefield matches notificationtype - Throws
ValidationErrorif validation fails
- Validates metadata against
-
Actions Validation
- Validates actions against
NotificationActionsSchema(generic array schema) - Converts
nulltoundefinedfor consistency
- Validates actions against
-
Title/Description Generation
- Generates title and description from metadata using
generateNotificationText() - Metadata is required for this step (throws error if missing)
- Generates title and description from metadata using
-
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
- Validates complete notification structure against type-specific schema from
-
Database Creation
- Creates notification record with validated data
- Repository assumes data is already validated (no additional validation)
Email Template Builder Flow
-
Parse Metadata (
parseNotificationMetadata)- Validates metadata against correct schema
- Returns typed metadata matching notification type
- Throws error if metadata type doesn't match notification type
-
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
- Maps notification type to email template via
-
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
env1EMAIL_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 notificationsPATCH /notifications/:id/seen- Mark notification as seenPATCH /notifications/seen-all- Mark all as seenDELETE /notifications/:id- Delete notification
Key Patterns
Title and Description Auto-Generation
Title and description are automatically generated from metadata - you never provide them:
typescript1// ✅ 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:
typescript1// ✅ 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):
typescript1// ✅ 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
failedand error message - Channel-level tracking: Each channel's status is tracked individually in
channelStatusJSONB field - Exponential backoff: Retries wait longer with each attempt (5min, 10min, 20min, 40min, 80min)
- Calculated as:
baseDelay * 2^retryCountwherebaseDelay = 5 minutes - Filtered in application code after querying from database
- Calculated as:
- Max 5 retries: Prevents infinite retry loops (retry count stored in
retryCountcolumn) - 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
sentorfailed, or all channels already processed)
Template System Details
React Email Components
Email templates are React components in packages/transactional/emails/:
- Use
@react-email/componentsfor email-safe components - Export prop types for type safety
- Use
BaseEmailwrapper for consistent layout - Include
PreviewPropsfor 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
- Backend calls
buildEmailTemplate()with typed metadata - Template builder calls
getEmailTemplate()(type-safe via overloading) - React component is rendered to HTML using
renderEmail()in transactional package - HTML is sent via email provider
Key Point: React stays in the transactional package - backend never imports React directly.
Best Practices
- Use helper functions for common notification types - they handle metadata structure correctly
- Keep notifications outside transactions - send after database operations complete
- Don't await notification creation - let it process in background (fire-and-forget)
- Use appropriate channels - email for important actions, in-app for updates (see Channel Selection Strategy)
- Include actions conditionally - some notifications may not need actions (e.g.,
notifySellerTicketSoldonly adds upload action if within time window) - Add metadata - store relevant IDs/context for future reference (required for title/description generation)
- Never provide title/description - they're auto-generated from metadata
- Export prop types - enables type safety in template mapping
- Follow discriminated union pattern - ensures metadata matches notification type
- Handle errors gracefully - notification failures are logged but don't break business logic
- Be conservative with emails - emails cost money; use
['in_app']only for informational updates - 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:
typescript1// 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
NotificationActionTypeenum (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.tsimplementation - 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
NotificationTypefrom shared) - Helper function created in
apps/backend/src/services/notifications/helpers.ts(optional but recommended) - API docs regenerated:
pnpm tsoa:both
Quick Commands
bash1# 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
FAQ & Installation Steps
These questions and steps mirror the structured data on this page for better search understanding.
? Frequently Asked Questions
What is revendiste-notification-system?
Ideal for Notification Agents requiring scalable and type-safe email notification systems with React Email support. Source code for revendiste.com
How do I install revendiste-notification-system?
Run the command: npx killer-skills add mathfalcon/revendiste/revendiste-notification-system. It works with Cursor, Windsurf, VS Code, Claude Code, and 19+ other IDEs.
What are the use cases for revendiste-notification-system?
Key use cases include: Sending automated email notifications for ticket sales, Generating order confirmation emails with custom templates, Triggering identity verification notifications upon completion or rejection.
Which IDEs are compatible with revendiste-notification-system?
This skill is compatible with Cursor, Windsurf, VS Code, Trae, Claude Code, OpenClaw, Aider, Codex, OpenCode, Goose, Cline, Roo Code, Kiro, Augment Code, Continue, GitHub Copilot, Sourcegraph Cody, and Amazon Q Developer. Use the Killer-Skills CLI for universal one-command installation.
Are there any limitations for revendiste-notification-system?
Requires TypeScript compatibility. Dependent on React Email for template rendering. Limited to notification services defined in the ~/services/notifications module.
↓ How To Install
-
1. Open your terminal
Open the terminal or command line in your project directory.
-
2. Run the install command
Run: npx killer-skills add mathfalcon/revendiste/revendiste-notification-system. The CLI will automatically detect your IDE or AI agent and configure the skill.
-
3. Start using the skill
The skill is now active. Your AI agent can use revendiste-notification-system immediately in the current project.