Notification
The NotificationModule provides two types of notification capabilities in the MBC CQRS Serverless framework:
- Real-time notifications via AWS AppSync for WebSocket-based updates
- Email notifications via AWS SES for sending emails
Architecture
Real-time Notifications
Overview
Real-time notifications are automatically sent when data changes occur in DynamoDB. The system uses AWS AppSync to deliver notifications to subscribed WebSocket clients.
INotification Interface
The notification payload structure:
interface INotification {
id: string; // Unique notification ID
table: string; // Source DynamoDB table name
pk: string; // Partition key of the changed item
sk: string; // Sort key of the changed item
tenantCode: string; // Tenant code for filtering notifications
action: string; // Type of change: 'INSERT', 'MODIFY', 'REMOVE'
content?: object; // Optional payload with changed data
}
AppSyncService
The AppSyncService sends real-time notifications to AppSync for WebSocket delivery.
Method: sendMessage(msg: INotification): Promise<any>
Sends a notification to AppSync via GraphQL mutation. The notification is delivered to all subscribed WebSocket clients.
await this.appSyncService.sendMessage({
id: "unique-id",
table: "my-table",
pk: "ITEM#tenant1",
sk: "ITEM#001",
tenantCode: "tenant1",
action: "MODIFY",
content: { status: "updated" },
});
Configuration
Set the following environment variables:
APPSYNC_ENDPOINT=https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
APPSYNC_API_KEY=da2-xxxxxxxxxx # Optional: Use API key auth instead of IAM
Usage
import { AppSyncService, INotification } from "@mbc-cqrs-serverless/core";
@Injectable()
export class MyService {
constructor(private readonly appSyncService: AppSyncService) {}
async notifyClients() {
const notification: INotification = {
id: "notification-123",
table: "my-table",
pk: "ITEM#tenant1",
sk: "ITEM#item001",
tenantCode: "tenant1",
action: "MODIFY",
content: { status: "updated" },
};
await this.appSyncService.sendMessage(notification);
}
}
Authentication
The AppSyncService supports two authentication methods:
- API Key: Set
APPSYNC_API_KEYenvironment variable - IAM Signature V4: Used automatically when API key is not set
Automatic Notifications
The framework automatically sends notifications when data changes through:
- DynamoDB Streams trigger the
NotificationEventHandler - Handler extracts change information and creates
INotification AppSyncService.sendMessage()delivers to AppSync- Connected clients receive updates via WebSocket subscription
NotificationEvent
The NotificationEvent class represents a notification event from SQS. It implements IEvent and wraps an SQS record containing notification data.
import { NotificationEvent } from "@mbc-cqrs-serverless/core";
class NotificationEvent implements IEvent, SQSRecord {
source: string;
messageId: string;
receiptHandle: string;
body: string; // JSON string containing INotification data
attributes: SQSRecordAttributes;
messageAttributes: SQSMessageAttributes;
md5OfBody: string;
eventSource: string;
eventSourceARN: string;
awsRegion: string;
// Creates a NotificationEvent from an SQS record
fromSqsRecord(record: SQSRecord): NotificationEvent;
}
NotificationEventHandler
The NotificationEventHandler is the built-in event handler that processes NotificationEvent and sends notifications to AppSync. It is automatically registered when using the notification module.
import { NotificationEventHandler, NotificationEvent } from "@mbc-cqrs-serverless/core";
@EventHandler(NotificationEvent)
export class NotificationEventHandler implements IEventHandler<NotificationEvent> {
async execute(event: NotificationEvent): Promise<any> {
// Parses the notification from event body
// Sends to AppSync via sendMessage()
}
}
You typically don't need to interact with this handler directly - it works automatically when notifications are published to the SQS queue.
AppSync Events API (opt-in)
AppSyncEventsService and dual-publish support were added in version 1.3.0.
Overview
The AppSyncEventsService provides an alternative (or complementary) real-time transport based on the AWS AppSync Events API — a schema-free HTTP pub/sub service. Unlike the GraphQL Subscription transport, it requires no GraphQL schema and clients subscribe using wildcard channel paths.
Opt in by setting NOTIFICATION_TRANSPORTS=appsync-event. When NOTIFICATION_TRANSPORTS=appsync-graphql,appsync-event and both endpoints are set, the framework dual-publishes to both transports simultaneously, enabling a zero-downtime migration.
Channel Structure
Every notification is published to a single most-specific channel. Clients subscribe at whatever level of granularity they need using the AppSync Events wildcard (/*):
/{namespace}/{tenantCode}/{action}/{sanitizedId}
seg 1 seg 2 seg 3 seg 4
| Client goal | Subscribe to |
|---|---|
| All events for a tenant | /{namespace}/{tenantCode}/* |
| Filtered by action | /{namespace}/{tenantCode}/{action}/* |
| Track one specific command | /{namespace}/{tenantCode}/{action}/{sanitizedId} |
Configuration
# Enable the Events API transport (Events API only)
NOTIFICATION_TRANSPORTS=appsync-event
APPSYNC_EVENTS_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/event
# Optional: must match a pre-created namespace in your AppSync Event API
APPSYNC_EVENTS_NAMESPACE=default
See AppSync Events Environment Variables for the full reference.
Migration from GraphQL Subscription
Use the dual-publish mode to migrate clients gradually without downtime:
Phase 1 — Dual-publish (both transports active):
NOTIFICATION_TRANSPORTS=appsync-graphql,appsync-event
APPSYNC_EVENTS_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/event
APPSYNC_EVENTS_NAMESPACE=default
APPSYNC_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql # still set
Phase 2 — Events API only (after all clients have migrated):
NOTIFICATION_TRANSPORTS=appsync-event
APPSYNC_EVENTS_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/event
APPSYNC_EVENTS_NAMESPACE=default
# APPSYNC_ENDPOINT removed
CDK Infrastructure
Add appsyncEvents to your Config to provision the EventApi and ChannelNamespace automatically:
// infra/config/config.ts
export const config: Config = {
// ...
appsyncEvents: {
enabled: true,
namespace: 'default', // optional, defaults to 'default'
apiKeyExpireDays: 365, // optional, defaults to 365
},
}
The CDK stack will output AppSyncEventsHttpEndpoint and AppSyncEventsNamespace, and will automatically inject NOTIFICATION_TRANSPORTS, APPSYNC_EVENTS_ENDPOINT, and APPSYNC_EVENTS_NAMESPACE into Lambda and ECS.
Authentication
The AppSyncEventsService supports two authentication methods for publishing:
- IAM SigV4 (recommended for Lambda/ECS): Used automatically for publishing. Lambda and ECS task roles are granted
appsync:EventPublishby the CDK stack viagrantPublish(). - API Key: Used by browser clients for subscribing. Set in the client (e.g., Amplify
apiKeyconfig). Not required on the server side.
Email Notifications
EmailService
The EmailService sends emails using AWS SES.
Configuration
SES_FROM_EMAIL=noreply@your-domain.com # Required: Default sender address
SES_REGION=ap-northeast-1 # Optional: SES region
SES_ENDPOINT= # Optional: Custom endpoint for LocalStack
Basic Usage
import { EmailService, EmailNotification } from "@mbc-cqrs-serverless/core";
@Injectable()
export class MyService {
constructor(private readonly emailService: EmailService) {}
async sendWelcomeEmail(userEmail: string) {
const email: EmailNotification = {
toAddrs: [userEmail],
subject: "Welcome to Our Service",
body: "<h1>Welcome!</h1><p>Thank you for signing up.</p>",
};
await this.emailService.sendEmail(email);
}
}
Email with Attachments
import { EmailNotification, Attachment } from "@mbc-cqrs-serverless/core";
import * as fs from "fs";
const pdfBuffer = fs.readFileSync("report.pdf");
const email: EmailNotification = {
toAddrs: ["user@example.com"],
subject: "Monthly Report",
body: "<p>Please find attached your monthly report.</p>",
attachments: [
{
filename: "report.pdf",
content: pdfBuffer,
contentType: "application/pdf",
},
],
};
await this.emailService.sendEmail(email);
Inline Template Emails
The sendInlineTemplateEmail() method allows you to send templated emails with dynamic data substitution, without requiring pre-registered SES templates.
Inline template emails (sendInlineTemplateEmail()) were added in version 1.0.23.
Basic Usage
import { EmailService, TemplatedEmailNotification } from "@mbc-cqrs-serverless/core";
@Injectable()
export class MyService {
constructor(private readonly emailService: EmailService) {}
async sendWelcomeEmail(user: { name: string; email: string }) {
const notification: TemplatedEmailNotification = {
toAddrs: [user.email],
template: {
subject: "Welcome, name!",
html: "<h1>Hello name</h1><p>Welcome to our service!</p>",
text: "Hello name, Welcome to our service!", // Optional plain text version
},
data: {
name: user.name,
},
};
await this.emailService.sendInlineTemplateEmail(notification);
}
}
Template Syntax
Templates use variableName placeholders that are replaced with values from the data object:
const notification: TemplatedEmailNotification = {
toAddrs: ["user@example.com"],
template: {
subject: "Order orderId Confirmation",
html: `
<h1>Thank you, customerName!</h1>
<p>Your order #orderId has been confirmed.</p>
<p>Total: currencytotalAmount</p>
`,
},
data: {
customerName: "John Doe",
orderId: "12345",
currency: "$",
totalAmount: "99.99",
},
};
Advanced Template Features
Nested property access and Unicode key support were added in version 1.0.25.
Nested Property Access
You can access nested object properties using dot notation:
const notification: TemplatedEmailNotification = {
toAddrs: ["user@example.com"],
template: {
subject: "Welcome user.profile.firstName!",
html: `
<p>Hello user.profile.firstName user.profile.lastName,</p>
<p>Your verification code is: auth.otp</p>
`,
},
data: {
user: {
profile: {
firstName: "John",
lastName: "Doe",
},
},
auth: {
otp: "123456",
},
},
};
Unicode and Japanese Key Support
Template variables support Unicode characters, including Japanese keys:
const notification: TemplatedEmailNotification = {
toAddrs: ["user@example.com"],
template: {
subject: "注文.確認番号 - Order Confirmation",
html: `
<p>顧客.名前 様</p>
<p>ご注文番号: 注文.確認番号</p>
<p>商品: 注文.詳細.品名</p>
`,
},
data: {
"顧客": {
"名前": "山田 太郎",
},
"注文": {
"確認番号": "ORD-2024-001",
"詳細": {
"品名": "ワイヤレスイヤホン",
},
},
},
};
Whitespace in Placeholders
Whitespace inside placeholders is automatically trimmed, so a placeholder with surrounding spaces is equivalent to one without (see the example below):
// Both of these work identically
template: {
subject: "Hello {{ name }}!", // Whitespace is trimmed
html: "<p>Hello name!</p>", // No whitespace
}
Missing Variables
If a variable is not found in the data object, the placeholder is preserved in the output. This helps identify missing data during development:
// If 'missingKey' is not in data, output will contain ' missingKey '
template: {
html: "<p>Value: missingKey</p>",
}
Limitations
The following limitations apply to template variable names (for security reasons):
| Limitation | Value | Reason |
|---|---|---|
| Maximum variable name length | 255 characters | Prevents ReDoS (Regular Expression Denial of Service) attacks |
Variable names exceeding 255 characters will not be replaced and will remain as literal placeholders in the output.
TemplatedEmailNotification Interface
| Property | Type | Required | Description |
|---|---|---|---|
fromAddr | string | No | Sender email (uses SES_FROM_EMAIL if not set) |
toAddrs | string[] | Yes | List of recipient email addresses |
ccAddrs | string[] | No | CC recipients |
bccAddrs | string[] | No | BCC recipients |
replyToAddrs | string[] | No | Reply-to addresses |
template | InlineTemplateContent | Yes | Template with subject, HTML, and optional text |
data | Record<string, any> | Yes | Data object for template variable substitution |
configurationSetName | string | No | SES configuration set name for tracking |
InlineTemplateContent Interface
| Property | Type | Required | Description |
|---|---|---|---|
subject | string | Yes | Email subject line (supports template variables) |
html | string | Yes | HTML body (supports template variables) |
text | string | No | Plain text body (supports template variables) |
Local Development
When running locally without SES access, the method automatically falls back to manual template compilation, allowing you to test email flows during development.
EmailNotification Interface
| Property | Type | Required | Description |
|---|---|---|---|
fromAddr | string | No | Sender email (uses SES_FROM_EMAIL if not set) |
toAddrs | string[] | Yes | List of recipient email addresses |
ccAddrs | string[] | No | CC recipients |
bccAddrs | string[] | No | BCC recipients |
subject | string | Yes | Email subject line |
body | string | Yes | Email body as HTML |
replyToAddrs | string[] | No | Reply-to addresses |
attachments | Attachment[] | No | File attachments |
emailTags | EmailTag[] | No | Tags for email categorization (SES Email Tags) |
Email Tags
Email tags allow you to categorize and track emails sent through AWS SES. Tags are useful for filtering emails in SES analytics, CloudWatch, and event destinations.
EmailTags support was added in version 1.1.0.
Basic Usage
import { EmailService, EmailNotification, EmailTag } from "@mbc-cqrs-serverless/core";
const email: EmailNotification = {
toAddrs: ["user@example.com"],
subject: "Order Confirmation",
body: "<p>Your order has been confirmed.</p>",
emailTags: [
{ name: "category", value: "order-confirmation" },
{ name: "tenant", value: "tenant-123" },
{ name: "environment", value: "production" },
],
};
await this.emailService.sendEmail(email);
EmailTag Interface
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tag name (e.g., 'category', 'campaign') |
value | string | Yes | Tag value for categorization |
Use Cases
- Campaign tracking: Tag emails by marketing campaign to analyze performance
- Tenant isolation: Tag by tenant code for multi-tenant email analytics
- Email type categorization: Distinguish transactional emails from promotional ones
- Environment tagging: Track emails across development, staging, and production
Attachment Interface
| Property | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Filename shown to recipient |
content | Buffer | Yes | File content as Buffer |
contentType | string | No | MIME type (e.g., 'application/pdf') |
Related Documentation
- Email Service - SES email sending
- Event Handling Patterns - How notifications integrate with events
- Environment Variables - AppSync and SES configuration
- Queue - SQS for notification queuing