@happyvertical/smrt-messages

Multi-channel messaging with STI hierarchies for Email, Slack, and Twitter. Credential encryption via smrt-secrets.

v0.20.44EmailSlackTwitter

Overview

smrt-messages provides unified multi-channel messaging with STI-based channel hierarchies. Messages and accounts each use single-table inheritance, so Email, SlackMessage, and Tweet all share a single messages table, while EmailAccount, SlackAccount, and TwitterAccount share a single accounts table.

Installation

bash
npm install @happyvertical/smrt-messages
# or
pnpm add @happyvertical/smrt-messages

Depends on @happyvertical/smrt-core, @happyvertical/smrt-secrets (credential encryption), and @happyvertical/email (SMTP/IMAP client).

Quick Start (5 Minutes)

1. Create an Email Account with Encrypted Credentials

typescript
import {
  Email, EmailCollection,
  EmailAccount, EmailAccountCollection,
  EmailSender, MessageCollection,
} from '@happyvertical/smrt-messages';

const accounts = new EmailAccountCollection(db);
const account = await accounts.create({
  name: 'Support',
  providerType: 'smtp',
});

// Credentials stored via smrt-secrets envelope encryption
await account.setCredentials({
  host: 'smtp.example.com',
  user: 'support@example.com',
  pass: process.env.SMTP_PASSWORD,
});

2. Send an Email

typescript
const emails = new EmailCollection(db);
const email = await emails.create({
  accountId: account.id,
  subject: 'Welcome',
  toAddresses: JSON.stringify([{ address: 'user@example.com' }]),
  textBody: 'Thanks for signing up!',
});

// Send lifecycle: draft -> sending -> sent (or failed)
const result = await email.send();

// Retry a failed message (respects maxRetries budget)
if (!result.success) {
  await email.retrySend();
}

3. Query Messages Across Channels

typescript
// Query all messages (Email, Slack, Twitter) via base collection
const messages = new MessageCollection(db);
const recent = await messages.list({
  orderBy: 'createdAt DESC',
  limit: 20,
});

Core Concepts

STI Message Hierarchy

All message types share a single messages table via STI (_meta_type discriminator):

TypeSTI DiscriminatorKey Fields
Message(base class)accountId, threadId, subject, body, fromAddress, toAddresses, sendStatus, retryCount
Email@happyvertical/smrt-messages:EmailmessageId (RFC 822), inReplyTo, ccAddresses, bccAddresses, htmlBody, textBody, folderId, labels, headers
Tweet@happyvertical/smrt-messages:TweettweetId, retweetCount, likeCount, mediaUrls, hashtags, mentions
SlackMessage@happyvertical/smrt-messages:SlackMessagechannelId, slackTs, slackThreadTs, reactions, blocks

STI Account Hierarchy

Accounts also use STI, sharing the accounts table:

TypeKey Fields
AccountproviderType, credentialSecretId, isActive, lastSyncAt, settings
EmailAccountSMTP/IMAP provider-specific fields + sync methods
SlackAccountSlack workspace connection
TwitterAccountTwitter API connection

Credential Security

Account credentials are stored via credentialSecretId pointing to smrt-secrets envelope encryption. Never store passwords as plain fields.

typescript
// Always use setCredentials/getCredentials
await account.setCredentials({
  host: 'smtp.example.com',
  user: 'support@example.com',
  pass: process.env.SMTP_PASSWORD,
});

const creds = await account.getCredentials();

Send Lifecycle

message.send() resolves the account, creates a provider-specific sender, and transitions through send statuses:

typescript
// Status flow: draft -> sending -> sent (or failed)
const result = await email.send();

// Each channel has a dedicated sender
// EmailSender, SlackSender, TweetSender
// All implement MessageSenderInterface

Email Filtering (Whitelist/Blacklist)

New in v0.20.42: Whitelist and Blacklist models for address-based email filtering.

typescript
import {
  Whitelist, WhitelistCollection,
  Blacklist, BlacklistCollection,
} from '@happyvertical/smrt-messages';

// Create whitelist/blacklist entries
const whitelist = new WhitelistCollection(db);
await whitelist.create({ address: 'trusted@example.com' });

const blacklist = new BlacklistCollection(db);
await blacklist.create({ address: 'spam@example.com' });

Per-Channel Senders

Each channel has a dedicated sender implementing MessageSenderInterface:

SenderDescription
EmailSenderSend emails via @happyvertical/email client
SlackSenderSend Slack messages via API
TweetSenderPost tweets via Twitter API

Svelte 5 Components

The package includes UI components for managing email accounts and filters:

  • EmailAccountManager: Admin UI for managing email account connections and credentials
  • EmailFilterManager: Admin UI for managing whitelist/blacklist rules
  • MessageCard, MessageList: Display message summaries
  • ComposeForm, ReplyForm, ForwardForm: Message composition
  • ThreadView, MessageDetail: Conversation display
  • FolderNav, MessageFilters: Navigation and filtering
  • AccountCard, AccountList, AccountAvatar: Account management
  • AttachmentChip, AttachmentUpload: Attachment handling
  • SendStatusBadge, MessageStatusIndicator, MessageTypeBadge: Status display
  • MessageToolbar, RecipientInput: Toolbar and input components
typescript
import {
  EmailAccountManager,
  EmailFilterManager,
  MessageCard,
  MessageList,
} from '@happyvertical/smrt-messages/svelte';

API Reference

Collections

typescript
// Base collections (query across all channels)
MessageCollection
AccountCollection
AttachmentCollection

// Email-specific collections
EmailCollection
EmailAccountCollection
EmailAttachmentCollection
EmailFolderCollection

// Email filtering
WhitelistCollection
BlacklistCollection

Best Practices

DOs

  • Use setCredentials()/getCredentials() for all account credentials
  • Use JSON address helpers: getToAddresses(), getCcAddresses()
  • Handle send failures with retry logic (maxRetries budget)
  • Use MessageCollection to query across all channel types
  • Store attachment references via messageId field

DON'Ts

  • Don't store passwords directly -- always use smrt-secrets encryption
  • Don't modify JSON fields directly -- use accessor methods (getX()/setX())
  • Don't override toJSON() -- use transformJSON()
  • Don't use emailId on Attachment -- use messageId (old field is deprecated)
  • Don't run concurrent syncs on the same account

Related Modules