Skip to content

Database Schema Reference

The chat-service uses PostgreSQL with Prisma 6 as the ORM. All models are defined in prisma/schema.prisma.

Enums

ChatChannelType

ValueDescription
DIRECT1-to-1 private conversation between two users
GROUPMulti-user group channel

ChatMessageType

ValueDescription
TEXTPlain text message
IMAGEImage attachment
VIDEOVideo attachment
AUDIOAudio attachment
FILEGeneric file attachment
ADMINSystem-generated admin message (e.g., "User X joined")
POLLMessage containing a poll reference

ChatMemberRole

ValueDescription
OPERATORCan manage channel settings, moderate users, invite/remove members
MEMBERStandard member with read/write access

ChatPushTrigger

ValueDescription
ALLReceive push notifications for all messages
MENTION_ONLYOnly receive push when mentioned
OFFNo push notifications for this channel

ChatCountPreference

ValueDescription
ALLCount all unread messages and mentions
UNREAD_MESSAGE_COUNT_ONLYOnly count unread messages (no mention tracking)
OFFDo not count unread messages for this channel

ChatScheduledStatus

ValueDescription
PENDINGWaiting to be sent at the scheduled time
SENTSuccessfully sent
FAILEDSending failed (see errorMessage field)
CANCELEDCanceled by the user before sending

ChatPollStatus

ValueDescription
OPENAccepting votes
CLOSEDVoting closed (manually or via closeAt expiry)

ChatReportCategory

ValueDescription
SPAMSpam or unwanted content
HARASSMENTHarassment or bullying
INAPPROPRIATEInappropriate or offensive content
OTHEROther reason (see description field)

Models

ChatChannel

The core channel model. Supports both direct (1:1) and group conversations.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
tenantIdString-NoTenant isolation key
typeChatChannelType-NoDIRECT or GROUP
nameString-YesDisplay name (typically null for direct channels)
coverUrlString-YesChannel cover image URL
customTypeString-YesApplication-defined type tag for filtering
isFrozenBooleanfalseNoWhen true, no new messages can be sent
metadataJson"{}"YesCustom key-value metadata store
lastMessageAtDateTime-YesTimestamp of the most recent message
memberCountInt0NoDenormalized count of active members
createdByIdString-NoUser ID of the channel creator
createdAtDateTimenow()NoCreation timestamp
updatedAtDateTime@updatedAtNoLast modification timestamp
deletedAtDateTime-YesSoft-delete timestamp (null = active)

Indexes:

  • [tenantId, updatedAt DESC] -- Channel list queries ordered by recent activity
  • [tenantId, type] -- Filter channels by type within a tenant
  • [deletedAt] -- Efficiently exclude soft-deleted channels

Relations:

  • members -> ChatChannelMember[] (one-to-many)
  • messages -> ChatMessage[] (one-to-many)
  • pinnedMessages -> ChatPinnedMessage[] (one-to-many)
  • scheduledMessages -> ChatScheduledMessage[] (one-to-many)

Design decisions:

  • memberCount is denormalized for performance (avoids counting joins on every channel list query).
  • lastMessageAt is denormalized to enable efficient "sort by last message" ordering.
  • Soft-delete via deletedAt preserves channel data for compliance/audit while hiding it from users.

ChatChannelMember

Join table between channels and users. Stores per-user preferences and moderation state.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
channelIdString-NoFK to ChatChannel.id
userIdString-NoExternal user ID
tenantIdString-NoTenant isolation key
roleChatMemberRoleMEMBERNoOPERATOR or MEMBER
isMutedBooleanfalseNoWhether the user is muted by an operator
mutedUntilDateTime-YesMute expiration (null = indefinite if muted)
isBannedBooleanfalseNoWhether the user is banned
bannedUntilDateTime-YesBan expiration (null = permanent if banned)
banDescriptionString-YesReason for the ban
isHiddenBooleanfalseNoChannel hidden from user's list
hidePrevMessagesBooleanfalseNoAlso hide messages before the hide action
pushTriggerChatPushTriggerALLNoPush notification preference
countPreferenceChatCountPreferenceALLNoUnread count preference
lastReadAtDateTime-YesWhen the user last read the channel
lastReadMessageIdString-YesID of the last message the user has read
lastDeliveredAtDateTime-YesWhen the last message was delivered to the user
historyResetAtDateTime-YesMessages before this timestamp are hidden
joinedAtDateTimenow()NoWhen the user joined the channel
leftAtDateTime-YesWhen the user left (null = still a member)
createdAtDateTimenow()NoRecord creation timestamp
updatedAtDateTime@updatedAtNoLast modification timestamp

Unique constraints:

  • [channelId, userId] -- A user can only be a member of a channel once

Indexes:

  • [userId, tenantId, leftAt] -- List channels for a user (excluding left channels)
  • [channelId, isBanned] -- Quickly find banned users in a channel
  • [channelId, isMuted] -- Quickly find muted users in a channel
  • [channelId, role] -- List operators in a channel

Relations:

  • channel -> ChatChannel (many-to-one, cascade delete)

Design decisions:

  • Read receipt cursor: lastReadAt + lastReadMessageId form a cursor-based read receipt system. The message ID provides precise tracking while the timestamp enables time-range queries.
  • History reset: historyResetAt allows users to "clear" their view of old messages without affecting other members. Messages before this timestamp are filtered out in queries.
  • Temporal bans/mutes: bannedUntil and mutedUntil support time-limited moderation. Application logic checks expiry and auto-clears the flag.
  • Soft membership: leftAt is used instead of deleting the record, preserving membership history for analytics and re-join scenarios.

ChatMessage

Individual chat messages within a channel.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
channelIdString-NoFK to ChatChannel.id
tenantIdString-NoTenant isolation key
senderIdString-NoExternal user ID of the sender
typeChatMessageTypeTEXTNoMessage content type
textString-YesMessage text content
fileUrlString-YesURL of attached file
fileNameString-YesOriginal filename
fileSizeInt-YesFile size in bytes
mimeTypeString-YesMIME type of attached file
thumbnailUrlString-YesThumbnail URL for images/videos
parentMessageIdString-YesParent message ID for threading
isForwardedBooleanfalseNoWhether this message was forwarded
forwardedFromIdString-YesOriginal message ID if forwarded
isEditedBooleanfalseNoWhether the message was edited
linkMetadataJson-YesLink preview data (URL, title, description, image)
mentionedUserIdsString[][]NoArray of mentioned user IDs
metadataJson-YesCustom application metadata
pollIdString-YesReference to a ChatPoll (for POLL-type messages)
createdAtDateTimenow()NoCreation timestamp
updatedAtDateTime@updatedAtNoLast modification timestamp
deletedAtDateTime-YesSoft-delete timestamp

Indexes:

  • [channelId, createdAt DESC] -- Default message list pagination
  • [channelId, deletedAt, createdAt DESC] -- Message list excluding deleted
  • [channelId, parentMessageId, createdAt DESC] -- Thread reply listing
  • [tenantId] -- Tenant-scoped queries
  • [senderId] -- Messages by a specific user
  • [pollId] -- Find the message associated with a poll

Relations:

  • channel -> ChatChannel (many-to-one, cascade delete)
  • reactions -> ChatReaction[] (one-to-many)

Design decisions:

  • Flat threading: Uses parentMessageId for single-level threading (replies to a parent, no nested threads).
  • Forwarding provenance: isForwarded + forwardedFromId tracks the chain of forwarded messages.
  • Denormalized mentions: mentionedUserIds is stored as a string array directly on the message for efficient mention queries without joins.
  • Soft-delete: Messages are soft-deleted via deletedAt rather than permanently removed.

ChatReaction

Emoji reactions on messages. Each user can add one reaction per key per message.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
messageIdString-NoFK to ChatMessage.id
userIdString-NoUser who reacted
keyString-NoReaction identifier (e.g., thumbsup, heart)
createdAtDateTimenow()NoWhen the reaction was added

Unique constraints:

  • [messageId, userId, key] -- One reaction per key per user per message

Indexes:

  • [messageId] -- All reactions for a message

Relations:

  • message -> ChatMessage (many-to-one, cascade delete)

ChatPinnedMessage

Pinned messages in a channel. Limited to 5 per channel (CHAT_DEFAULTS.MAX_PINNED_MESSAGES).

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
channelIdString-NoFK to ChatChannel.id
messageIdString-NoThe pinned message ID
pinnedByIdString-NoUser who pinned the message
createdAtDateTimenow()NoWhen the message was pinned

Unique constraints:

  • [channelId, messageId] -- A message can only be pinned once per channel

Indexes:

  • [channelId] -- List pinned messages in a channel

Relations:

  • channel -> ChatChannel (many-to-one, cascade delete)

ChatScheduledMessage

Messages scheduled to be sent at a future time. Processed by a BullMQ worker.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
channelIdString-NoFK to ChatChannel.id
tenantIdString-NoTenant isolation key
senderIdString-NoUser who scheduled the message
typeChatMessageTypeTEXTNoMessage type
textString-YesMessage text
fileUrlString-YesAttached file URL
fileNameString-YesAttached file name
fileSizeInt-YesAttached file size in bytes
mimeTypeString-YesAttached file MIME type
thumbnailUrlString-YesThumbnail URL
mentionedUserIdsString[][]NoMentioned user IDs
metadataJson-YesCustom metadata
scheduledAtDateTime-NoWhen to send the message
statusChatScheduledStatusPENDINGNoCurrent status
sentMessageIdString-YesID of the actual message after sending
errorMessageString-YesError details if sending failed
createdAtDateTimenow()NoCreation timestamp
updatedAtDateTime@updatedAtNoLast modification timestamp

Indexes:

  • [channelId, status, scheduledAt] -- Find pending messages for a channel ordered by send time
  • [status, scheduledAt] -- BullMQ worker: find all pending messages due for sending
  • [senderId] -- List scheduled messages by a user

Relations:

  • channel -> ChatChannel (many-to-one, cascade delete)

Design decisions:

  • The status state machine is: PENDING -> SENT | FAILED | CANCELED. Only PENDING messages can be updated or sent.
  • sentMessageId links back to the actual ChatMessage created when the scheduled message is dispatched.
  • The BullMQ queue (chat-scheduled-messages) polls the [status, scheduledAt] index to find due messages.

ChatPoll

Polls embedded in channel messages.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
channelIdString-NoChannel where the poll was created
tenantIdString-NoTenant isolation key
titleString-NoPoll question/title
allowMultipleVotesBooleanfalseNoWhether users can vote on multiple options
allowUserSuggestionBooleanfalseNoWhether users can add new options
closeAtDateTime-YesAuto-close timestamp (null = no auto-close)
statusChatPollStatusOPENNoOPEN or CLOSED
voterCountInt0NoDenormalized total unique voter count
createdByIdString-NoUser who created the poll
createdAtDateTimenow()NoCreation timestamp
updatedAtDateTime@updatedAtNoLast modification timestamp

Indexes:

  • [channelId] -- Find polls in a channel

Relations:

  • options -> ChatPollOption[] (one-to-many)

Design decisions:

  • voterCount is denormalized to avoid counting distinct voters across options on every read.
  • A ChatMessage with type: POLL and pollId referencing this model is created alongside the poll, so the poll appears inline in the message timeline.

ChatPollOption

Individual options within a poll.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
pollIdString-NoFK to ChatPoll.id
textString-NoOption text
voteCountInt0NoDenormalized vote count
positionInt0NoDisplay order
createdAtDateTimenow()NoCreation timestamp
updatedAtDateTime@updatedAtNoLast modification timestamp

Indexes:

  • [pollId] -- List options for a poll

Relations:

  • poll -> ChatPoll (many-to-one, cascade delete)
  • votes -> ChatPollVote[] (one-to-many)

ChatPollVote

Individual votes on poll options. Each user can vote once per option.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
optionIdString-NoFK to ChatPollOption.id
userIdString-NoUser who voted
createdAtDateTimenow()NoWhen the vote was cast

Unique constraints:

  • [optionId, userId] -- One vote per user per option

Indexes:

  • [userId] -- Find all votes by a user (useful for checking if they already voted)

Relations:

  • option -> ChatPollOption (many-to-one, cascade delete)

ChatUserBlock

User-to-user blocking within a tenant.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
tenantIdString-NoTenant isolation key
blockerIdString-NoUser who initiated the block
blockedIdString-NoUser who was blocked
createdAtDateTimenow()NoWhen the block was created

Unique constraints:

  • [blockerId, blockedId] -- A user can only block another user once

Indexes:

  • [blockerId, tenantId] -- List users blocked by a specific user
  • [blockedId, tenantId] -- Check if a user is blocked by anyone (used for direct channel creation filtering)

Design decisions:

  • Blocking is directional: if A blocks B, B can still see A unless B also blocks A.
  • Blocking prevents creating new direct channels and hides existing direct channels from the blocked user's list.

ChatReport

User-submitted reports for moderation review.

FieldTypeDefaultNullableDescription
idString (CUID)cuid()NoPrimary key
tenantIdString-NoTenant isolation key
reporterIdString-NoUser who filed the report
categoryChatReportCategory-NoReport category
descriptionString-YesAdditional details from the reporter
targetTypeString-NoType of reported entity: "channel", "user", or "message"
targetIdString-NoID of the reported entity
channelIdString-YesChannel context (for message reports)
createdAtDateTimenow()NoWhen the report was submitted

Indexes:

  • [tenantId, targetType] -- List reports by type within a tenant (admin moderation dashboard)
  • [reporterId] -- Find all reports by a specific user

Design decisions:

  • targetType is a string discriminator ("channel", "user", "message") rather than separate tables, keeping the reporting system flexible and simple.
  • Reports are write-only from the user perspective. Moderation review is handled externally.

Entity Relationship Diagram

ChatChannel 1──* ChatChannelMember
ChatChannel 1──* ChatMessage
ChatChannel 1──* ChatPinnedMessage
ChatChannel 1──* ChatScheduledMessage

ChatMessage 1──* ChatReaction

ChatPoll    1──* ChatPollOption
ChatPollOption 1──* ChatPollVote

ChatMessage ..> ChatPoll (via pollId)

Tenant Isolation

All models include a tenantId field ensuring strict data isolation between tenants. Queries always include tenantId in their WHERE clause. Key indexes are prefixed with tenantId to ensure efficient tenant-scoped queries:

  • ChatChannel: [tenantId, updatedAt], [tenantId, type]
  • ChatChannelMember: [userId, tenantId, leftAt]
  • ChatMessage: [tenantId]
  • ChatUserBlock: [blockerId, tenantId], [blockedId, tenantId]
  • ChatReport: [tenantId, targetType]

Constants

Default limits defined in src/core/constants/chat.constants.ts:

ConstantValueDescription
MAX_CHANNEL_MEMBERS100Maximum members per channel
MAX_PINNED_MESSAGES5Maximum pinned messages per channel
MAX_MESSAGE_LENGTH5000Maximum characters per message
MAX_POLL_OPTIONS10Maximum options per poll
MAX_FILE_SIZE25 MB (26,214,400 bytes)Maximum file upload size
MESSAGE_PAGE_SIZE30Default messages per page
CHANNEL_PAGE_SIZE20Default channels per page
TYPING_TIMEOUT_MS5000Typing indicator timeout
CHAT_CACHE_TTL30 secondsRedis cache TTL

MIT License