Messages & Channels
Messages represent immutable communication between entities within Stoneforge. They function similarly to email - once sent, they cannot be edited or deleted, providing a reliable audit trail.
Message Interface
interface Message extends Element {
readonly type : 'message' ;
// Location
channelId : ChannelId ; // Mutable for channel merge operations
// Sender
readonly sender : EntityId ;
// Content
readonly contentRef : DocumentId ; // Reference to content Document
readonly attachments : readonly DocumentId []; // Attachment Documents
// Threading
readonly threadId : MessageId | null ; // Parent message (null for root)
}
Channel containing this message (mutable for channel merge operations)
Entity that sent the message
Reference to content Document (messages don’t store raw text)
References to attachment Documents (max 100 attachments)
Parent message for threading (null for root messages)
Immutability
Messages are immutable after creation:
updatedAt always equals createdAt
Cannot be edited or deleted
Provides reliable audit trail
// Attempting to update or delete throws MessageImmutableError
rejectMessageUpdate ( messageId : string ): never
rejectMessageDelete ( messageId : string ): never
// Verify immutability constraint
verifyImmutabilityConstraint ( message : Message ): boolean
Messages cannot be updated or deleted. Design your message system accordingly - use correction messages or replies instead of editing.
Creating Messages
interface CreateMessageInput {
channelId : ChannelId ;
sender : EntityId ;
contentRef : DocumentId ;
// Optional fields
attachments ?: DocumentId [];
threadId ?: MessageId | null ;
tags ?: string [];
metadata ?: Record < string , unknown >;
}
// Create a new message
await createMessage (
input : CreateMessageInput ,
config ?: IdGeneratorConfig
): Promise < Message >
Show Example: Send Message
import { createMessage , asEntityId } from '@stoneforge/core' ;
const message = await createMessage ({
channelId: channel . id ,
sender: asEntityId ( 'el-user' ),
contentRef: contentDoc . id ,
attachments: [ attachment1 . id , attachment2 . id ],
threadId: null , // Root message
});
API/service layer is responsible for validating channel exists, sender is member, and documents are valid.
Message Threading
Root Messages vs. Replies
// Check if message is a root message
isRootMessage ( message : Message ): boolean
// Check if message is a reply
isReply ( message : Message ): boolean
Thread Utilities
// Get all messages in a thread (root + replies, sorted by time)
getThreadMessages < T extends Message > (
messages : T [],
rootMessageId : MessageId
): T []
// Filter messages by thread
filterByThread < T extends Message >( messages : T [], threadId : MessageId ) : T []
// Filter root messages only
filterRootMessages < T extends Message >( messages : T []) : T []
// Filter reply messages only
filterReplies < T extends Message >( messages : T []) : T []
Attachments
// Check if message has attachments
hasAttachments ( message : Message ): boolean
// Get attachment count
getAttachmentCount ( message : Message ): number
Maximum number of attachments per message
Filtering Messages
// Filter by channel
filterByChannel < T extends Message >( messages : T [], channelId : ChannelId ) : T []
// Filter by sender
filterBySender < T extends Message >( messages : T [], sender : EntityId ) : T []
// Filter by thread
filterByThread < T extends Message >( messages : T [], threadId : MessageId ) : T []
// Filter root messages
filterRootMessages < T extends Message >( messages : T []) : T []
// Filter replies
filterReplies < T extends Message >( messages : T []) : T []
Sorting Messages
// Sort by creation time (oldest first)
sortByCreatedAt < T extends Message >( messages : T []) : T []
// Sort by creation time (newest first)
sortByCreatedAtDesc < T extends Message >( messages : T []) : T []
Grouping Messages
// Group by channel
groupByChannel < T extends Message >( messages : T []) : Map < ChannelId , T []>
// Group by sender
groupBySender < T extends Message >( messages : T []) : Map < EntityId , T []>
Message Utilities
// Check if message was sent by specific entity
isSentBy ( message : Message , entityId : EntityId ): boolean
// Check if message is in specific channel
isInChannel ( message : Message , channelId : ChannelId ): boolean
// Check if message is in specific thread
isInThread ( message : Message , threadId : MessageId ): boolean
Type Guards
// Check if value is a valid Message
isMessage ( value : unknown ): value is Message
// Comprehensive validation with detailed errors
validateMessage ( value : unknown ): Message
Channels
Channels are containers for messages, organizing communication into logical groups. They support both direct messaging (1:1) and group conversations.
Channel Interface
interface Channel extends Element {
readonly type : 'channel' ;
// Content
readonly name : string ;
readonly description : string | null ;
// Channel Type
readonly channelType : 'direct' | 'group' ;
// Membership
readonly members : readonly EntityId [];
// Permissions
readonly permissions : ChannelPermissions ;
}
interface ChannelPermissions {
readonly visibility : 'public' | 'private' ;
readonly joinPolicy : 'open' | 'invite-only' | 'request' ;
readonly modifyMembers : readonly EntityId [];
}
Channel Types
Direct Channels
Group Channels
1:1 communication between two entities:
Exactly 2 members (immutable)
Deterministic name (sorted entity IDs joined by colon)
Always private visibility
Always invite-only join policy
No one can modify membership
// Generate direct channel name
generateDirectChannelName ( entityA : EntityId , entityB : EntityId ): string
// Returns: "entityA:entityB" (sorted)
// Parse entity IDs from name
parseDirectChannelName ( name : string ): [ EntityId , EntityId ] | null
Multi-party communication:
2-1000 members
Custom name
Configurable visibility (public/private)
Configurable join policy
Designated members can modify membership
Creating Channels
Direct Channel
interface CreateDirectChannelInput {
entityA : EntityId ;
entityB : EntityId ;
createdBy : EntityId ; // Must be entityA or entityB
// Optional fields
entityAName ?: string ; // Display name for entity A
entityBName ?: string ; // Display name for entity B
description ?: string | null ;
tags ?: string [];
metadata ?: Record < string , unknown >;
}
// Create direct channel
await createDirectChannel (
input : CreateDirectChannelInput ,
config ?: IdGeneratorConfig
): Promise < Channel >
Show Example: Create Direct Channel
import { createDirectChannel , asEntityId } from '@stoneforge/core' ;
const channel = await createDirectChannel ({
entityA: asEntityId ( 'el-alice' ),
entityB: asEntityId ( 'el-bob' ),
createdBy: asEntityId ( 'el-alice' ),
entityAName: 'alice' ,
entityBName: 'bob' ,
});
// Channel name will be: "alice:bob" (sorted alphabetically)
Group Channel
interface CreateGroupChannelInput {
name : string ;
createdBy : EntityId ;
// Optional fields
members ?: EntityId []; // Creator automatically included
description ?: string | null ;
visibility ?: 'public' | 'private' ; // default: private
joinPolicy ?: 'open' | 'invite-only' | 'request' ; // default: invite-only
modifyMembers ?: EntityId []; // Creator automatically included
tags ?: string [];
metadata ?: Record < string , unknown >;
}
// Create group channel
await createGroupChannel (
input : CreateGroupChannelInput ,
config ?: IdGeneratorConfig
): Promise < Channel >
Show Example: Create Group Channel
import { createGroupChannel , asEntityId } from '@stoneforge/core' ;
const channel = await createGroupChannel ({
name: 'team-alpha' ,
createdBy: asEntityId ( 'el-alice' ),
members: [
asEntityId ( 'el-alice' ),
asEntityId ( 'el-bob' ),
asEntityId ( 'el-charlie' ),
],
visibility: 'public' ,
joinPolicy: 'open' ,
description: 'Team Alpha coordination channel' ,
});
Channel Membership
Membership Checks
// Check if entity is a member
isMember ( channel : Channel , entityId : EntityId ): boolean
// Check if entity can modify membership
canModifyMembers ( channel : Channel , entityId : EntityId ): boolean
// Check if entity can join
canJoin ( channel : Channel , entityId : EntityId ): boolean
// Get member count
getMemberCount ( channel : Channel ): number
Membership Errors
// Thrown when attempting to modify direct channel membership
class DirectChannelMembershipError extends ConstraintError
// Thrown when entity is not a member
class NotAMemberError extends ConstraintError
// Thrown when entity lacks permission
class CannotModifyMembersError extends ConstraintError
Channel Permissions
Visibility
const VisibilityValue = {
PUBLIC: 'public' , // Discoverable by all
PRIVATE: 'private' , // Invitation only
} as const ;
// Check visibility
isPublicChannel ( channel : Channel ): boolean
isPrivateChannel ( channel : Channel ): boolean
Join Policy
const JoinPolicyValue = {
OPEN: 'open' , // Anyone can join (if public)
INVITE_ONLY: 'invite-only' , // Must be added by modifier
REQUEST: 'request' , // Can request to join (approval needed)
} as const ;
Filtering Channels
// Filter by type
filterByChannelType < T extends Channel >( channels : T [], channelType : ChannelType ) : T []
filterDirectChannels < T extends Channel >( channels : T []) : T []
filterGroupChannels < T extends Channel >( channels : T []) : T []
// Filter by member
filterByMember < T extends Channel >( channels : T [], entityId : EntityId ) : T []
// Filter by visibility
filterByVisibility < T extends Channel >( channels : T [], visibility : Visibility ) : T []
filterPublicChannels < T extends Channel >( channels : T []) : T []
filterPrivateChannels < T extends Channel >( channels : T []) : T []
Finding Channels
// Find direct channel between two entities
findDirectChannel < T extends Channel > (
channels : T [],
entityA : EntityId ,
entityB : EntityId
): T | undefined
// Get all direct channels for an entity
getDirectChannelsForEntity < T extends Channel > (
channels : T [],
entityId : EntityId
): T []
Sorting Channels
// Sort by name
sortByName < T extends Channel >( channels : T []) : T []
// Sort by member count
sortByMemberCount < T extends Channel >( channels : T []) : T []
// Sort by creation time (newest first)
sortByCreatedAtDesc < T extends Channel >( channels : T []) : T []
Grouping Channels
// Group by visibility
groupByVisibility < T extends Channel >( channels : T []) : Map < Visibility , T []>
// Group by type
groupByChannelType < T extends Channel >( channels : T []) : Map < ChannelType , T []>
Validation Constants
Minimum channel name length
Maximum channel name length
Maximum members in a channel
Minimum members for a group channel
Exact member count for direct channels
Type Guards
// Check if value is a valid Channel
isChannel ( value : unknown ): value is Channel
// Check channel type
isDirectChannel ( channel : Channel ): boolean
isGroupChannel ( channel : Channel ): boolean
// Comprehensive validation
validateChannel ( value : unknown ): Channel
validateDirectChannelConstraints ( channel : Channel ): boolean
Best Practices
Messages Are Immutable Design your system around immutable messages. Use replies or correction messages instead of editing.
Content as Documents Always store message content in Document elements and reference via contentRef.
Direct Channel Naming Use generateDirectChannelName() for consistent, deterministic naming.
Membership Validation Validate sender is a channel member before creating messages.