Skip to main content

Events & Notifications

The Ever Works API uses the NestJS EventEmitterModule for internal event-driven communication and a REST-based notification system for delivering messages to users. There is no WebSocket gateway; all real-time-like behavior is achieved through event-driven processing on the server and client-side polling via the notification REST API.

Architecture Overview

Event Producers (Services)
├── AuthService -- User lifecycle events
├── WorkOperationsService -- Work generation events
├── WorkImportService -- Import completion events
├── WorkGenerationService -- Generation completion events
├── PluginRegistryService -- Plugin lifecycle events
├── PluginLifecycleManager -- Plugin load/unload events
└── PluginSettingsService -- Plugin settings change events

EventEmitterModule (in-process event bus)

Event Consumers (Listeners)
├── MailService -- Sends transactional emails
├── WorkCleanupService -- Clears cache on generation complete
└── NotificationService -- Creates persistent notifications

Notification Delivery
└── NotificationsController -- REST API for client polling

Event System Configuration

The event emitter is registered globally in ApiModule:

import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
imports: [
EventEmitterModule.forRoot()
// ... other modules
]
})
export class ApiModule {}

This enables any service to emit events via the injected EventEmitter2 instance and any service to listen via @OnEvent() decorators.

Event Classes

Base Classes

Two base classes define the event hierarchy:

API Events (apps/api/src/events/index.ts):

export abstract class BaseUserEvent {
public abstract user: User;
}

Agent Events (packages/agent/src/events/base.ts):

export abstract class BaseEvent {
static EVENT_NAME: string;
}

User Lifecycle Events

All user events extend BaseUserEvent and are defined in apps/api/src/events/index.ts. They are emitted by AuthService and consumed by MailService.

Event ClassEvent NameTriggerConsumer
UserCreatedEventuser.createdUser registrationSends signup confirmation email
UserConfirmedEventuser.confirmedEmail verification or OAuth signupSends welcome email
UserForgotPasswordEventuser.forgot_passwordPassword reset requestSends forgot-password email
UserPasswordChangedEventuser.password_changedPassword changeSends password-changed alert
UserNewDeviceLoginEventuser.new_device_loginLogin from unrecognized deviceSends new-device alert
UserAccountDeletionEventuser.delete_accountAccount deletion requestSends deletion confirmation email
MemberInvitedEventwork.member_invitedUser invited to a workSends invitation email

Work Events

Work events extend BaseEvent and are defined in packages/agent/src/events/.

Event ClassEvent NameTriggerConsumer
WorkCreatedEventwork.createdNew work createdInternal processing
WorkGenerationCompletedEventwork.generation.completedAI generation finishesCache cleanup

Plugin Lifecycle Events

Plugin events use string constants from PluginEvents in packages/agent/src/plugins/plugins.constants.ts:

export const PluginEvents = {
LOADED: 'plugin:loaded',
UNLOADED: 'plugin:unloaded',
ERROR: 'plugin:error',
SETTINGS_CHANGED: 'plugin:settings-changed',
STATE_CHANGED: 'plugin:state-changed',
REGISTERED: 'plugin:registered',
UNREGISTERED: 'plugin:unregistered'
} as const;
EventEmitted ByPayload
plugin:loadedPluginLifecycleManager{ pluginId, metadata }
plugin:unloadedPluginLifecycleManager{ pluginId }
plugin:errorPluginLifecycleManager{ pluginId, error }
plugin:settings-changedPluginSettingsService{ pluginId, userId, changes }
plugin:state-changedPluginRegistryService{ pluginId, oldState, newState }
plugin:registeredPluginRegistryService{ pluginId, metadata }
plugin:unregisteredPluginRegistryService{ pluginId }

Event Producers

AuthService

The AuthService emits user lifecycle events during authentication flows:

// User registration
this.eventEmitter.emit(UserCreatedEvent.EVENT_NAME, new UserCreatedEvent(user, verificationToken, callbackUrl));

// Email verification
this.eventEmitter.emit(
UserConfirmedEvent.EVENT_NAME,
new UserConfirmedEvent(updatedUser, `${this.webAppUrl}/works/new`)
);

// Password reset request
this.eventEmitter.emit(
UserForgotPasswordEvent.EVENT_NAME,
new UserForgotPasswordEvent(user, resetToken, callbackUrl, '1 hour')
);

OAuth sign-ups (GitHub, Google) also emit UserConfirmedEvent after creating a new user, bypassing the email verification step.

WorkOperationsService

Emits WorkGenerationCompletedEvent when AI content generation finishes for a work. This triggers cache invalidation in WorkCleanupService.

WorkImportService

Emits WorkGenerationCompletedEvent after importing work content from external sources. Also emits WorkCreatedEvent when a new work is created during import.

Event Consumers

MailService (Email Sender)

Source: apps/api/src/mail/mail.service.ts

The primary event consumer. Listens to all 7 user lifecycle events and sends the corresponding transactional email using Handlebars templates.

@OnEvent(UserCreatedEvent.EVENT_NAME)
async sendSignupConfirmation(data: UserCreatedEvent): Promise<void> { ... }

@OnEvent(UserForgotPasswordEvent.EVENT_NAME)
async sendForgotPassword(data: UserForgotPasswordEvent): Promise<void> { ... }

@OnEvent(UserPasswordChangedEvent.EVENT_NAME)
async sendPasswordChanged(data: UserPasswordChangedEvent): Promise<void> { ... }

@OnEvent(UserConfirmedEvent.EVENT_NAME)
async sendWelcomeEmail(data: UserConfirmedEvent): Promise<void> { ... }

@OnEvent(UserNewDeviceLoginEvent.EVENT_NAME)
async sendNewDeviceAlert(data: UserNewDeviceLoginEvent): Promise<void> { ... }

@OnEvent(UserAccountDeletionEvent.EVENT_NAME)
async sendAccountDeletionConfirmation(data: UserAccountDeletionEvent): Promise<void> { ... }

@OnEvent(MemberInvitedEvent.EVENT_NAME)
async sendMemberInvitation(data: MemberInvitedEvent): Promise<void> { ... }

Each handler wraps the email send in a try-catch and logs failures without throwing, ensuring a failed email does not disrupt the originating operation.

WorkCleanupService (Cache Invalidation)

Source: apps/api/src/works/tasks/work-cleanup.service.ts

Listens for generation completion to clear cached data:

@OnEvent(WorkGenerationCompletedEvent.EVENT_NAME)
clearWorkCache(data: WorkGenerationCompletedEvent) {
this.cacheRepository.typeormAdapter
.deleteUnscopedEntriesLike(data.work.id)
.then(() => this.logger.log(`Cache cleared for work ${data.work.id}`))
.catch((err) => this.logger.error('Failed to clear cache:', err));
}

This service also runs a scheduled job every 10 minutes to detect stalled generations and mark them as errors.

Notification System

Instead of WebSockets, the platform uses a database-backed notification system with REST polling.

Notification Entity

Source: packages/agent/src/entities/notification.entity.ts

@Entity({ name: 'notifications' })
@Index(['userId', 'isRead'])
@Index(['userId', 'deduplicationKey'], { unique: true, where: '"deduplicationKey" IS NOT NULL' })
export class Notification {
id: string; // UUID primary key
userId: string; // Owner
type: NotificationType; // info, warning, error, success
category: NotificationCategory;
title: string; // Up to 200 chars
message: string; // Full text
actionUrl?: string; // Deep link
actionLabel?: string; // Button label
metadata?: Record<string, any>;
isRead: boolean;
isDismissed: boolean;
isPersistent: boolean; // Cannot be dismissed
createdAt: Date;
expiresAt?: Date;
deduplicationKey?: string;
}

Notification Types and Categories

TypeDescription
infoInformational messages
warningWarnings requiring attention
errorErrors requiring action
successSuccess confirmations
CategoryDescription
ai_creditsAI credit depletion and provider errors
subscriptionSubscription-related notifications
generationWork generation status
systemSystem-level messages
securitySecurity alerts (auth expiration)

NotificationService

Source: packages/agent/src/notifications/notification.service.ts

Provides CRUD operations with deduplication and lifecycle management:

MethodDescription
create(dto)Create notification with deduplication
getNotifications(userId, options)Query with filters
getUnreadCount(userId)Count unread
markAsRead(userId, id)Mark single as read
markAllAsRead(userId)Mark all as read
dismiss(userId, id)Hide from view (not persistent)
getPersistentNotifications(userId)Get critical alerts
clearByDeduplicationKey(userId, key)Clear resolved issues
cleanup()Delete expired/old notifications

Convenience methods for common notification scenarios:

MethodCategoryPersistent
notifyAiCreditsDepleted(userId, provider)ai_creditsYes
notifyAiProviderError(userId, provider, message)ai_creditsNo
notifyGenerationAccountError(userId, workId, name, message)generationNo
notifySchedulePaused(userId, workId, name, reason)generationNo
notifyGitAuthExpired(userId, provider)securityYes

Deduplication

Notifications support deduplication via an optional deduplicationKey. When a key is provided, the service checks for an existing non-dismissed notification with the same key before creating a new one. Race conditions are handled by catching unique constraint violations:

async create(dto: CreateNotificationDto): Promise<Notification> {
if (dto.deduplicationKey) {
const existing = await this.repository.findByDeduplicationKey(
dto.userId, dto.deduplicationKey,
);
if (existing && !existing.isDismissed) return existing;
}
// ... create with race condition handling
}

REST API Endpoints

Source: apps/api/src/notifications/notifications.controller.ts

All endpoints require JWT authentication and are under /api/notifications.

MethodEndpointDescription
GET/api/notificationsList notifications with filters
GET/api/notifications/unread-countGet unread count
GET/api/notifications/persistentGet critical alerts
POST/api/notifications/:id/readMark as read
POST/api/notifications/read-allMark all as read
POST/api/notifications/:id/dismissDismiss notification

Query Parameters for listing:

ParameterTypeDefaultDescription
unreadOnlybooleanfalseFilter to unread only
limitnumber50Max results (capped at 100)
offsetnumber0Pagination offset
categorystring--Filter by category

Event Flow Diagrams

User Registration Flow

AuthService.register()
--> emit(UserCreatedEvent)
--> MailService.sendSignupConfirmation()
--> Sends "signup-confirmation" email with verification link

AuthService.verifyEmail()
--> emit(UserConfirmedEvent)
--> MailService.sendWelcomeEmail()
--> Sends "welcome" email with dashboard link

Work Generation Flow

WorkGenerationService.generate()
--> AI content generation completes
--> emit(WorkGenerationCompletedEvent)
--> WorkCleanupService.clearWorkCache()
--> Clears all cache entries for the work

WorkCleanupService.handleStalledGenerations() [every 10 min]
--> Finds works stuck in GENERATING for too long
--> Marks them as ERROR status

Plugin Settings Change Flow

PluginSettingsService.upsertSettings()
--> Persists settings to database
--> emit(PluginEvents.SETTINGS_CHANGED)
--> Plugin system reacts to configuration changes

Source Files

FilePurpose
apps/api/src/events/index.tsUser lifecycle event definitions
apps/api/src/mail/mail.service.tsEmail event consumers
apps/api/src/works/tasks/work-cleanup.service.tsGeneration event consumer
apps/api/src/notifications/notifications.controller.tsNotification REST API
apps/api/src/auth/services/auth.service.tsUser event emitter
apps/api/src/api.module.tsEventEmitterModule registration
packages/agent/src/events/base.tsBase event class
packages/agent/src/events/work-generation-completed.event.tsGeneration completed event
packages/agent/src/events/work-created.event.tsWork created event
packages/agent/src/plugins/plugins.constants.tsPlugin event constants
packages/agent/src/notifications/notification.service.tsNotification business logic
packages/agent/src/entities/notification.entity.tsNotification database entity
packages/agent/src/entities/notification.types.tsNotification types and enums