Adding a reaction that already exists for a user is idempotent. Removing the last user from an emoji key deletes the key entirely.
Threading
Messages support two threading fields:
reply_to_id -- References the specific message being replied to (inline reply)
thread_root_id -- References the root message of a thread (groups all replies under one parent)
Use GET /v1/conversations/:id/threads/:msgId to fetch all messages in a thread by passing the root message ID.
Message Editing
Messages can be edited within the configured edit window (CHAT_EDIT_WINDOW_MINUTES). When a message is edited:
The original content is preserved in metadata.original_content
The edited_at timestamp is set
The content field is updated to the new text
The edit window is enforced server-side. If CHAT_EDIT_WINDOW_MINUTES is 0, editing is unlimited. The sender_id query parameter must match the original sender.
Message Deletion
Deleting a message is a soft delete:
The deleted_at timestamp is set
The content is replaced with [deleted]
The message record remains in the database for audit purposes
Moderators can delete any message via POST /v1/moderate with action: "delete_message", which replaces content with [deleted by moderator].
Read Receipts & Unread Counts
When a message is sent, unread counts are incremented for all participants in the conversation except the sender. When a user marks a conversation as read via POST /v1/conversations/:id/read:
The last_read_message_id and last_read_at are updated
The unread_count is recalculated by counting messages after the read position
Search
The search endpoint supports multiple filters that can be combined:
Filter
Type
Description
content
string
Case-insensitive substring match (ILIKE)
sender_id
string
Filter by sender
content_type
string
Filter by content type
has_attachments
boolean
Filter messages with/without attachments
is_pinned
boolean
Filter pinned messages
from_date
ISO date
Messages created on or after this date
to_date
ISO date
Messages created on or before this date
Database Schema
np_chat_conversations
Column
Type
Description
id
UUID (PK)
Conversation ID
source_account_id
VARCHAR(128)
Multi-app isolation
type
VARCHAR(16)
direct, group, channel
name
VARCHAR(255)
Conversation display name
description
TEXT
Conversation description
avatar_url
TEXT
Conversation avatar URL
created_by
VARCHAR(255)
User who created the conversation
is_archived
BOOLEAN
Whether conversation is archived
is_muted
BOOLEAN
Whether conversation is muted
last_message_at
TIMESTAMPTZ
Timestamp of last message
last_message_preview
TEXT
Preview of last message (first 100 chars)
message_count
INTEGER
Total message count
member_count
INTEGER
Active participant count
metadata
JSONB
Arbitrary metadata
created_at
TIMESTAMPTZ
Creation timestamp
updated_at
TIMESTAMPTZ
Last update
np_chat_participants
Column
Type
Description
id
UUID (PK)
Participant record ID
source_account_id
VARCHAR(128)
Multi-app isolation
conversation_id
UUID (FK)
References np_chat_conversations
user_id
VARCHAR(255)
User ID
role
VARCHAR(32)
owner, admin, moderator, member, guest
nickname
VARCHAR(128)
Display nickname in this conversation
muted_until
TIMESTAMPTZ
Muted expiration (null = not muted)
joined_at
TIMESTAMPTZ
When user joined
left_at
TIMESTAMPTZ
When user left (null = active)
last_read_message_id
UUID
Last message read by this user
last_read_at
TIMESTAMPTZ
When user last read
notification_level
VARCHAR(32)
all, mentions, none
created_at
TIMESTAMPTZ
Creation timestamp
Unique constraint on (conversation_id, user_id). Re-adding a removed participant clears left_at via upsert.
np_chat_messages
Column
Type
Description
id
UUID (PK)
Message ID
source_account_id
VARCHAR(128)
Multi-app isolation
conversation_id
UUID (FK)
References np_chat_conversations
sender_id
VARCHAR(255)
Message sender
reply_to_id
UUID (FK)
Inline reply reference
thread_root_id
UUID (FK)
Thread root message reference
content
TEXT
Message text content
content_type
VARCHAR(32)
text, image, file, audio, video, system, embed
attachments
JSONB
Array of attachment objects
mentions
TEXT[]
Array of mentioned user IDs
reactions
JSONB
Map of emoji to user ID arrays
edited_at
TIMESTAMPTZ
When message was edited
deleted_at
TIMESTAMPTZ
When message was soft-deleted
is_pinned
BOOLEAN
Whether message is pinned
pinned_at
TIMESTAMPTZ
When message was pinned
pinned_by
VARCHAR(255)
Who pinned the message
metadata
JSONB
Arbitrary metadata (includes original_content after edit)
created_at
TIMESTAMPTZ
Creation timestamp
np_chat_read_receipts
Column
Type
Description
id
UUID (PK)
Receipt ID
source_account_id
VARCHAR(128)
Multi-app isolation
conversation_id
UUID (FK)
References np_chat_conversations
user_id
VARCHAR(255)
User ID
last_read_message_id
UUID (FK)
Last read message reference
last_read_at
TIMESTAMPTZ
When user last read
unread_count
INTEGER
Number of unread messages
Unique constraint on (conversation_id, user_id). Upserted on each read receipt update.
np_chat_moderation_actions
Column
Type
Description
id
UUID (PK)
Action ID
source_account_id
VARCHAR(128)
Multi-app isolation
conversation_id
UUID (FK)
Target conversation
message_id
UUID (FK)
Target message (for delete_message)
target_user_id
VARCHAR(255)
User receiving the action
moderator_id
VARCHAR(255)
Moderator who took the action
action
VARCHAR(32)
delete_message, warn, mute, kick, ban
reason
TEXT
Reason for the action
duration_minutes
INTEGER
Duration for timed actions (mute)
created_at
TIMESTAMPTZ
Action timestamp
np_chat_webhook_events
Standard webhook event tracking table with id, source_account_id, event_type, payload (JSONB), processed, processed_at, error, created_at.
Database Indexes
The schema creates the following indexes for query performance:
Index
Columns
Notes
idx_chat_conversations_source_account
source_account_id
Multi-app filtering
idx_chat_conversations_type
type
Filter by conversation type
idx_chat_conversations_created_by
created_by
Find conversations by creator
idx_chat_conversations_created_at
created_at DESC
Chronological listing
idx_chat_conversations_last_message_at
last_message_at DESC NULLS LAST
Sort by activity
idx_chat_conversations_archived
is_archived (partial)
Active conversations only
idx_chat_participants_conversation
conversation_id
Participant lookups
idx_chat_participants_user
user_id
User's conversations
idx_chat_participants_active
(conversation_id, user_id) (partial)
Active participants only
idx_chat_messages_conversation_created
(conversation_id, created_at DESC)
Message listing per conversation
idx_chat_messages_sender
sender_id
Messages by sender
idx_chat_messages_thread_root
thread_root_id (partial)
Thread lookups
idx_chat_messages_reply_to
reply_to_id (partial)
Reply chain lookups
idx_chat_messages_pinned
(conversation_id, is_pinned) (partial)
Pinned message listing
idx_chat_messages_deleted
deleted_at (partial)
Deleted message tracking
Troubleshooting
Messages not appearing -- Verify the conversation ID is correct and the sender is a participant. Check message content length against CHAT_MAX_MESSAGE_LENGTH.
Edit window expired -- The server enforces CHAT_EDIT_WINDOW_MINUTES from the message's created_at. Increase the value or set to 0 for unlimited editing.
Participant not found -- Participants with left_at set are not returned by default. Re-adding a removed participant clears left_at via upsert.
Pinned message limit reached -- The server enforces CHAT_MAX_PINNED per conversation. Unpin existing messages before pinning new ones.
Unread count incorrect -- Run POST /v1/conversations/:id/read with the latest last_read_message_id to recalculate. Unread counts are incremented on each new message and recalculated on each read receipt update.
Search returns no results -- Search uses case-insensitive ILIKE matching. Deleted messages (with deleted_at set) are still returned in search results. Use the from_date and to_date filters to narrow the time range.
Reactions not updating -- Reactions are stored as a JSONB map on the message record. Adding the same reaction twice for the same user is idempotent. The emoji in the DELETE URL must be URL-encoded.