Skip to main content

Communication Recipient Targeting System

Overview​

This document describes the flexible recipient targeting system that allows businesses to control WHO receives each type of communication through any channel (email, SMS, WhatsApp).

Problem Statement​

Previously, the system could only send communications to:

  • All business users (no filtering)
  • Specific individual recipients (one at a time)

Businesses need:

  1. βœ… Send to specific roles (e.g., all managers, all inventory managers)
  2. βœ… Create custom groups with mixed users, emails, and phone numbers
  3. βœ… Include ad-hoc recipients (external emails/phones not in the system)
  4. βœ… Combine all the above (roles + groups + ad-hoc for one communication type)

Architecture​

Core Tables​

1. communication_recipient_group​

Custom groups businesses can create to organize recipients.

Example Groups:

  • "Finance Team" (3 finance managers + external accountant email)
  • "Purchasing Department" (inventory managers + 2 supplier contacts)
  • "Emergency Contacts" (owners + security service phone numbers)
CREATE TABLE communication_recipient_group (
id UUID PRIMARY KEY,
business_id UUID NOT NULL REFERENCES business(id),
name VARCHAR NOT NULL, -- "Finance Team"
description TEXT, -- Purpose/details
code VARCHAR, -- Optional system code
is_active BOOLEAN DEFAULT TRUE,
tags TEXT[], -- Organization/filtering
created_by UUID REFERENCES user(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(business_id, name)
);

2. communication_group_member​

Members of recipient groups (polymorphic: users OR emails OR phones).

Member Types:

  • business_user: Link to existing business user
  • email: External email address
  • phone: Phone number for SMS
  • whatsapp: WhatsApp number
CREATE TYPE recipient_member_type AS ENUM (
'business_user', 'email', 'phone', 'whatsapp'
);

CREATE TABLE communication_group_member (
id UUID PRIMARY KEY,
group_id UUID NOT NULL REFERENCES communication_recipient_group(id),
member_type recipient_member_type NOT NULL,

-- Polymorphic references (only ONE filled based on member_type)
business_user_id UUID REFERENCES business_user(id),
email_address VARCHAR,
phone_number VARCHAR,
display_name VARCHAR,

is_active BOOLEAN DEFAULT TRUE,
added_by UUID REFERENCES user(id),
added_at TIMESTAMPTZ DEFAULT NOW()
);

3. business_communication_recipient_rule​

Links communication configs to recipient targeting rules.

Targeting Types:

  • role: All users with a specific role
  • group: All members of a custom group
  • ad_hoc_email: Single email address
  • ad_hoc_phone: Single phone number
  • ad_hoc_whatsapp: Single WhatsApp number
CREATE TYPE recipient_targeting_type AS ENUM (
'role', 'group', 'ad_hoc_email', 'ad_hoc_phone', 'ad_hoc_whatsapp'
);

CREATE TABLE business_communication_recipient_rule (
id UUID PRIMARY KEY,
business_id UUID NOT NULL,
communication_type communication_type NOT NULL,
channel communication_channel NOT NULL,

targeting_type recipient_targeting_type NOT NULL,

-- Polymorphic references (only ONE filled based on targeting_type)
role_name unique_role_names, -- For 'role' type
group_id UUID REFERENCES communication_recipient_group(id),
ad_hoc_email VARCHAR,
ad_hoc_phone VARCHAR,
ad_hoc_whatsapp VARCHAR,
ad_hoc_name VARCHAR,

priority INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);

4. communication_recipient_log​

Audit trail of recipient selection (for compliance and debugging).

CREATE TABLE communication_recipient_log (
id UUID PRIMARY KEY,
communication_id UUID REFERENCES communication(id),

recipient_type VARCHAR NOT NULL,
recipient_id UUID,
recipient_contact VARCHAR NOT NULL,
recipient_name VARCHAR,

selection_method VARCHAR, -- 'role', 'group', 'ad_hoc'
selection_source_id UUID, -- Which rule selected them
selection_details JSON,

selected_at TIMESTAMPTZ DEFAULT NOW()
);

Data Flow​

Example: Low Stock Alert​

graph TB
A[Low Stock Alert Created] --> B[RecipientResolverService]
B --> C{Query Recipient Rules}
C --> D[Rule 1: role = 'inventory_manager']
C --> E[Rule 2: group = 'Purchasing Team']
C --> F[Rule 3: ad_hoc_email = 'supplier@vendor.com']

D --> G[Resolve: Get all inventory managers]
E --> H[Resolve: Get all group members]
F --> I[Resolve: Use email directly]

G --> J[Deduplicate Recipients]
H --> J
I --> J

J --> K[Send Communication to Each]
K --> L[Log Recipients Selected]

Usage Examples​

Example 1: Low Stock Alerts​

Requirement: Send low stock alerts to:

  • All inventory managers (role)
  • Purchasing team group (3 staff + 1 external supplier email)
  • Owner's personal phone (ad-hoc)

Configuration:

-- 1. Create Purchasing Team group
INSERT INTO communication_recipient_group (business_id, name, description)
VALUES ('business-123', 'Purchasing Team', 'Team responsible for inventory purchasing');

-- 2. Add members to group
INSERT INTO communication_group_member (group_id, member_type, business_user_id)
VALUES ('group-456', 'business_user', 'user-1'),
('group-456', 'business_user', 'user-2'),
('group-456', 'business_user', 'user-3');

INSERT INTO communication_group_member (group_id, member_type, email_address, display_name)
VALUES ('group-456', 'email', 'supplier@vendor.com', 'Main Supplier Contact');

-- 3. Create recipient rules for low_stock_alert
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name, priority)
VALUES ('business-123', 'low_stock_alert', 'email', 'role', 'inventory_manager', 1);

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, group_id, priority)
VALUES ('business-123', 'low_stock_alert', 'email', 'group', 'group-456', 2);

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, ad_hoc_phone, ad_hoc_name, priority)
VALUES ('business-123', 'low_stock_alert', 'sms', 'ad_hoc_phone', '+1234567890', 'Owner Mobile', 3);

Result:

  • Email sent to all inventory managers
  • Email sent to 3 purchasing staff + supplier
  • SMS sent to owner's phone

Example 2: Daily Sales Reports​

Requirement: Send daily reports to:

  • Finance group (accountant + 2 managers)
  • Owner
  • External accountant email
-- 1. Create Finance group
INSERT INTO communication_recipient_group (business_id, name)
VALUES ('business-123', 'Finance Group');

-- 2. Add members
INSERT INTO communication_group_member (group_id, member_type, business_user_id)
VALUES ('finance-group', 'business_user', 'accountant-user'),
('finance-group', 'business_user', 'manager-1'),
('finance-group', 'business_user', 'manager-2');

-- 3. Create rules
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, group_id)
VALUES ('business-123', 'daily_report', 'email', 'group', 'finance-group');

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name)
VALUES ('business-123', 'daily_report', 'email', 'role', 'owner');

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, ad_hoc_email, ad_hoc_name)
VALUES ('business-123', 'daily_report', 'email', 'ad_hoc_email', 'cpa@accounting-firm.com', 'External CPA');

Example 3: Emergency Alerts (Multi-Channel)​

Requirement: Send emergency alerts via BOTH email AND SMS to:

  • All owners
  • All admins
  • Security service phone
  • Manager group
-- Email channel rules
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name)
VALUES ('business-123', 'emergency_alert', 'email', 'role', 'owner'),
('business-123', 'emergency_alert', 'email', 'role', 'admin');

-- SMS channel rules
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name)
VALUES ('business-123', 'emergency_alert', 'sms', 'role', 'owner'),
('business-123', 'emergency_alert', 'sms', 'role', 'admin');

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, ad_hoc_phone, ad_hoc_name)
VALUES ('business-123', 'emergency_alert', 'sms', 'ad_hoc_phone', '+1-555-SECURITY', 'Security Service');

Implementation Guide​

Step 1: Create RecipientResolverService​

@Injectable()
export class RecipientResolverService {
constructor(
private recipientRuleRepository: RecipientRuleRepository,
private recipientGroupRepository: RecipientGroupRepository,
private businessUsersService: BusinessUsersService,
) {}

/**
* Resolve all recipients for a communication
*/
async resolveRecipients(
businessId: string,
communicationType: CommunicationType,
channel: CommunicationChannel,
additionalFilters?: RecipientFilters,
): Promise<ResolvedRecipient[]> {
// 1. Get all active recipient rules for this business + type + channel
const rules = await this.recipientRuleRepository.findActiveRules({
businessId,
communicationType,
channel,
});

// 2. Resolve each rule to actual recipients
const recipientSets = await Promise.all(
rules.map((rule) => this.resolveRule(rule, additionalFilters)),
);

// 3. Flatten and deduplicate
const allRecipients = recipientSets.flat();
const deduplicated = this.deduplicateRecipients(allRecipients);

// 4. Apply any additional filters (e.g., location-specific)
return this.applyFilters(deduplicated, additionalFilters);
}

/**
* Resolve a single rule to recipients
*/
private async resolveRule(
rule: RecipientRule,
filters?: RecipientFilters,
): Promise<ResolvedRecipient[]> {
switch (rule.targetingType) {
case 'role':
return this.resolveRoleRecipients(rule.roleName, rule.businessId);

case 'group':
return this.resolveGroupRecipients(rule.groupId);

case 'ad_hoc_email':
return [{
type: 'email',
contact: rule.adHocEmail,
name: rule.adHocName,
source: { ruleId: rule.id, method: 'ad_hoc' },
}];

case 'ad_hoc_phone':
return [{
type: 'phone',
contact: rule.adHocPhone,
name: rule.adHocName,
source: { ruleId: rule.id, method: 'ad_hoc' },
}];

case 'ad_hoc_whatsapp':
return [{
type: 'whatsapp',
contact: rule.adHocWhatsapp,
name: rule.adHocName,
source: { ruleId: rule.id, method: 'ad_hoc' },
}];
}
}

/**
* Resolve role to all users with that role
*/
private async resolveRoleRecipients(
roleName: string,
businessId: string,
): Promise<ResolvedRecipient[]> {
const users = await this.businessUsersService.findByRole(businessId, roleName);

return users.map((user) => ({
type: 'business_user',
userId: user.userId,
contact: user.email, // or phone based on channel
name: user.fullName,
source: { method: 'role', roleName },
}));
}

/**
* Resolve group to all members
*/
private async resolveGroupRecipients(
groupId: string,
): Promise<ResolvedRecipient[]> {
const members = await this.recipientGroupRepository.findGroupMembers(groupId);

return members.map((member) => {
if (member.memberType === 'business_user') {
return {
type: 'business_user',
userId: member.businessUserId,
contact: member.user.email, // Join with user table
name: member.user.fullName,
source: { method: 'group', groupId },
};
} else {
return {
type: member.memberType,
contact: member.emailAddress || member.phoneNumber,
name: member.displayName,
source: { method: 'group', groupId },
};
}
});
}

/**
* Deduplicate recipients (same person via multiple rules)
*/
private deduplicateRecipients(
recipients: ResolvedRecipient[],
): ResolvedRecipient[] {
const seen = new Map<string, ResolvedRecipient>();

for (const recipient of recipients) {
const key = `${recipient.type}:${recipient.contact}`;
if (!seen.has(key)) {
seen.set(key, recipient);
}
}

return Array.from(seen.values());
}
}

Step 2: Update Event Handlers​

@Injectable()
export class OnCreateLowStockAlertHandler {
constructor(
private readonly communicationsService: CommunicationsService,
private readonly recipientResolverService: RecipientResolverService,
) {}

async handleOnCreateLowStockAlert(event: OnCreateLowStockAlertEvent) {
const { createdLowStockAlertRecord } = event;

// Resolve recipients based on rules
const recipients = await this.recipientResolverService.resolveRecipients(
createdLowStockAlertRecord.businessId,
'low_stock_alert',
'email', // or iterate through all enabled channels
);

// Send to each recipient
for (const recipient of recipients) {
await this.communicationsService.send({
businessId: createdLowStockAlertRecord.businessId,
channel: 'email',
type: 'low_stock_alert',
recipientType: recipient.type,
recipientId: recipient.userId,
recipientContact: recipient.contact,
// ... other fields
});
}
}
}

Step 3: Create Management APIs​

Group Management​

  • POST /api/recipient-groups - Create group
  • GET /api/recipient-groups?businessId=xxx - List groups
  • PUT /api/recipient-groups/:id - Update group
  • DELETE /api/recipient-groups/:id - Delete group
  • POST /api/recipient-groups/:id/members - Add member
  • DELETE /api/recipient-groups/:id/members/:memberId - Remove member

Recipient Rule Management​

  • POST /api/communication-recipient-rules - Create rule
  • GET /api/communication-recipient-rules?businessId=xxx&type=low_stock_alert - List rules
  • PUT /api/communication-recipient-rules/:id - Update rule
  • DELETE /api/communication-recipient-rules/:id - Delete rule
  • POST /api/communication-recipient-rules/preview - Preview recipients for a rule

Benefits​

1. Flexibility​

  • Mix roles, groups, and ad-hoc recipients
  • Different targeting per communication type
  • Different targeting per channel

2. Scalability​

  • Add new communication types without code changes
  • Businesses configure their own rules
  • Easy to extend with new targeting types

3. Maintainability​

  • Clear separation of concerns
  • Audit trail of recipient selection
  • Easy to debug why someone received/didn't receive a message

4. Business Control​

  • Business admins manage their own groups
  • No developer involvement for new recipients
  • Self-service configuration

Migration Path​

For Existing Systems​

  1. Create tables (run migration)

  2. Create default rules for existing communications:

    -- Convert "send to all" to "send to admin + owner roles"
    INSERT INTO business_communication_recipient_rule
    (business_id, communication_type, channel, targeting_type, role_name)
    SELECT DISTINCT business_id, 'low_stock_alert', 'email', 'role', 'admin'
    FROM business;
  3. Deploy RecipientResolverService

  4. Update event handlers one at a time

  5. Build admin UI for self-service management

Future Enhancements​

  1. Location-based filtering - Send only to users at specific locations
  2. Time-based rules - Send to different people at different times
  3. Escalation chains - If no response, escalate to next group
  4. Recipient preferences - Let users opt-out of specific communication types
  5. Smart groups - Dynamic groups based on criteria (e.g., "all managers at locations with low stock")
  6. Templates per group - Different message templates for different recipient groups

Database Diagram​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ business_communication_config β”‚
β”‚ (type + channel settings) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”‚ 1:N
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ business_communication_ │◄─────┐
β”‚ recipient_rule β”‚ β”‚
β”‚ (WHO receives this type) β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
role β”‚ β”‚ group β”‚ ad_hoc
β”‚ β”‚ β”‚
β–Ό β–Ό β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ Roles β”‚ β”‚ recipient_ β”‚ β”‚
β”‚ (enum) β”‚ β”‚ group β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ 1:N β”‚
β–Ό β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ group_member β”‚β”€β”€β”€β”€β”˜
β”‚ (users/emails/ β”‚
β”‚ phones) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Conclusion​

This recipient targeting system provides a flexible, scalable solution for controlling who receives communications in your business. It supports role-based, group-based, and ad-hoc targeting with full auditability and easy maintenance.

The polymorphic design allows for future extensions without schema changes, and the service-based resolution makes it easy to integrate with any communication event handler.