Skip to main content

@happyvertical/smrt-messages

Unified multi-channel messaging with persona routes, encrypted credentials, and provider adapters. Outbound delivery supports email, Zulip, and Telegram; SMS is a reserved channel ready for a provider adapter.

Installation

pnpm add @happyvertical/smrt-messages

Usage

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

// Create an email account with encrypted credentials
const accounts = await EmailAccountCollection.create({ db });
const account = await accounts.create({
name: 'Support',
providerType: 'smtp',
});
await account.setCredentials({
host: 'smtp.example.com',
user: 'support@example.com',
pass: process.env.SMTP_PASSWORD,
});

// Send an email through the send lifecycle
const emails = await EmailCollection.create({ db });
const email = await emails.create({
accountId: account.id,
subject: 'Welcome',
toAddresses: JSON.stringify([{ address: 'user@example.com' }]),
textBody: 'Thanks for signing up!',
});
const result = await email.send();
// sendStatus transitions: draft -> sending -> sent (or failed)

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

// Query messages across all channels via base collection
const messages = await MessageCollection.create({ db });
const recent = await messages.list({ orderBy: 'createdAt DESC', limit: 20 });

API

Models (STI Hierarchy)

Messages and accounts each use single-table inheritance. STI discriminators use qualified names (e.g., @happyvertical/smrt-messages:Email).

ExportDescription
MessageBase message with send lifecycle (draft/sending/sent/failed)
EmailEmail with RFC 822 threading, cc/bcc, htmlBody/textBody, folders
SlackMessageSlack message with channelId, slackTs, reactions, blocks
TweetTwitter message with tweetId, retweetCount, likeCount, hashtags
AccountBase account with setCredentials()/getCredentials() via smrt-secrets
EmailAccountEmail account (SMTP/IMAP provider)
SlackAccountSlack workspace account
TwitterAccountTwitter API account
AttachmentMessage attachment
EmailAttachmentEmail-specific attachment
EmailFolderEmail folder/label

Collections

MessageCollection, EmailCollection, AccountCollection, EmailAccountCollection, AttachmentCollection, EmailAttachmentCollection, EmailFolderCollection

Senders

Each channel has a dedicated sender implementing MessageSenderInterface.

ExportDescription
EmailSenderSend emails via @happyvertical/email client
SlackSenderSend Slack messages via API
TweetSenderPost tweets via Twitter API
ZulipSenderSend stream or private Zulip messages via bot credentials
TelegramSenderSend Telegram bot messages to a chat/topic

Persona-scoped outbound messaging

MessagingSettingsService is the permission-gated server boundary for saving accounts, write-only credentials, destinations, and persona routes. PersonaMessagingService selects the highest-priority matching route, persists the message, then runs the normal send lifecycle.

Schema changes are manifest-driven. After upgrading an application, run smrt db:diff and smrt db:migrate to add the account/message columns and the messaging_endpoints / persona_message_routes tables.

When constructing MessagingSettingsService, provide resolvePersonaTenantId(personaId) from the host's persona collection. Route writes fail closed unless it proves the persona belongs to the same tenant.

import {
messagingPrincipalFromPermissions,
PersonaMessagingService,
} from '@happyvertical/smrt-messages';

const messaging = new PersonaMessagingService({
db,
tenantId,
principal: messagingPrincipalFromPermissions(['messages.send'], { tenantId }),
});

await messaging.send({
personaId,
channel: 'telegram', // optional; default route otherwise
body: 'The scheduled report is ready.',
});

Agents receive the persona-fixed tool from @happyvertical/smrt-personas:

const tool = createPersonaMessagingTool({ db, tenantId, personaId });

The tool only accepts message content and route intent. Persona, account, endpoint, and credential ids remain trusted server state.

Apps can render MessagingSettingsPanel from @happyvertical/smrt-messages/svelte using the sanitized view returned by MessagingSettingsService.list() and wiring its save callbacks back to the service.

Credential Security

Account credentials are stored via credentialSecretId pointing to smrt-secrets envelope encryption. Use account.setCredentials() and account.getCredentials() -- never store passwords as plain fields.

Key Types

MessageType, ProviderType, SendStatus, SendMessageOptions, SendEmailOptions, SendSlackOptions, SendTweetOptions, MessageSendResult, MessageSenderInterface, SyncOptions, SyncResult, SyncProgress

Dependencies

  • @happyvertical/smrt-core -- ORM and code generation
  • @happyvertical/smrt-secrets -- credential encryption (envelope encryption)
  • @happyvertical/smrt-tenancy -- optional multi-tenant scoping
  • @happyvertical/smrt-types -- shared type definitions
  • @happyvertical/email -- SMTP/IMAP email client
  • Peer: @happyvertical/smrt-svelte (optional)

Contributor guide

See AGENTS.md for package architecture, invariants, validation, and contributor guidance.