Technical Docs

CollabChat Technical Documentation Internal

Complete API reference and architecture guide for engineers and support teams.

Architecture

CollabChat is a monolithic Node.js application with real-time WebSocket support, PostgreSQL persistence via Drizzle ORM, and a plain HTML/CSS/vanilla JS frontend.

Stack

Backend Node.js + Express v4, TypeScript, Drizzle ORM, ws (WebSocket), multer (uploads), Resend (email)
Frontend Plain HTML, CSS, vanilla JavaScript — no framework, no build step. "Tight Dark" UI kit.
Database PostgreSQL with Drizzle schema management (db:push). Session store in connect-pg-simple.
File Storage Cloudflare R2 (S3-compatible). Bucket: colab. Falls back to local-only if R2 env vars are missing.

Request Flow

HTTP requests go through Express middleware (session, passport, JSON parser) then to route handlers. WebSocket connections authenticate via the sid session cookie on upgrade. Static files are served from public/ with the uploads directory served from public/uploads/ (or proxied from R2).

Database Schema

All tables use UUID primary keys via gen_random_uuid() unless noted. Timestamps default to now().

Core Tables

users id (PK), email (unique), display_name, first_name, last_name, profile_image_url, custom_role, tagline, role (owner/admin/user), is_admin (int 0/1), is_active (int 0/1), created_at
sessions sid (PK, varchar), sess (JSON), expire (timestamp). Managed by connect-pg-simple.
otp_codes id (PK), email, code (6-digit), expires_at, created_at
invites id (PK), email, name, invited_by (FK → users), created_at

Messaging Tables

channels id (PK), name, description, is_private (int 0/1), created_by (FK), created_at
channel_members id (PK), channel_id (FK), user_id (FK), joined_at
dm_conversations id (PK), created_at
dm_members id (PK), conversation_id (FK), user_id (FK), joined_at
messages id (PK), content (text), image_urls (text, comma-separated), user_id (FK), channel_id (FK, nullable), dm_conversation_id (FK, nullable), parent_message_id (FK, nullable — for threads), reply_count (int), link_previews (text, JSON array), is_visible (int 0/1), is_commentable (int 0/1), edited_at, created_at
message_edits id (PK), message_id (FK), user_id (FK), original_content, original_image_urls, edited_at
read_status id (PK), user_id (FK), channel_id (FK, nullable), dm_conversation_id (FK, nullable), last_read_at

Access Control Tables

groups id (PK), name, created_by (FK), created_at
group_members id (PK), group_id (FK), user_id (FK), created_at
channel_groups id (PK), channel_id (FK), group_id (FK), created_at

Social Tables

user_follows id (PK), follower_id (FK), following_id (FK), created_at. Unique constraint on (follower_id, following_id).
feed_seen id (PK), user_id (FK), message_id (FK), seen_at. Unique constraint on (user_id, message_id).

Authentication Flow (Email + OTP)

CollabChat uses a passwordless email + one-time-password flow. Users must have an active invite before they can register.

Step 1: Request OTP

POST /api/request-otp Sends a 6-digit OTP to the provided email via Resend. OTP expires in 10 minutes.
ParamTypeDescription
emailstringEmail address (normalized to lowercase, trimmed)

If the email has no invite record and no existing user account, the request is rejected with a 403.

Step 2: Verify OTP

POST /api/verify-otp Validates the OTP. If correct and not expired, creates or retrieves the user, establishes a session, and returns the user object.
ParamTypeDescription
emailstringEmail used in step 1
codestring6-digit OTP code

On success, the user gets a session cookie (sid) and is logged in. Inactive users are rejected with a 403.

Session Check

GET /api/me Returns the current authenticated user object, or 401 if not logged in.

Logout

POST /api/logout Destroys the session and clears the sid cookie.
First user bootstrap The very first user to verify an OTP becomes the owner and is automatically set as admin. All subsequent users must be invited first.

Invite System

Account creation is invite-only. Any authenticated user can invite others.

POST /api/invite Auth Creates an invite record and sends an invitation email via Resend.
ParamTypeDescription
emailstringInvitee's email (normalized to lowercase)
namestringOptional display name for the invitee
GET /api/invites Auth Lists all invites created by the current user.
Duplicate prevention If an invite already exists for that email, a new invite is not created but the email is re-sent. If the user already has an account, a 400 is returned.

Sessions

Sessions are stored in the sessions PostgreSQL table via connect-pg-simple. The session cookie is named sid, is httpOnly, and uses sameSite lax. Session max age is 30 days.

Passport.js handles serialization: the user ID is stored in the session, and the full user record is fetched on each request via deserializeUser.

Force logout When an admin deactivates a user, all their sessions are deleted from the database and a force-logout WebSocket event is broadcast to that user.

Channels

GET /api/channels Auth Lists all channels visible to the current user. For non-admin users, only channels they are a member of are returned. Admins see all channels. Response includes member counts.
POST /api/channels Admin Creates a new channel. The creator is auto-added as a member.
ParamTypeDescription
namestringChannel name (trimmed)
descriptionstringOptional description
isPrivatenumber0 = public, 1 = private. Default 0
PATCH /api/channels/:id Admin Updates channel name and/or description.
DELETE /api/channels/:id Admin Deletes a channel and all associated data (messages, memberships, read status). Broadcasts a force-refresh event.
POST /api/channels/:id/join Auth Joins a public channel. Private or group-restricted channels cannot be joined this way.

Direct Messages

GET /api/dm Auth Lists all DM conversations for the current user, including member details and a computed display name (the other user's name).
POST /api/dm Auth Creates a DM conversation with another user, or returns the existing one if it already exists.
ParamTypeDescription
userIdstringThe other user's ID. Cannot DM yourself.
Deduplication The server checks all existing conversations before creating a new one. If both users already share a DM conversation, that conversation ID is returned instead.

Inline Comment Threads

Any top-level message can have threaded replies. Threads are identified by the parentMessageId field on reply messages.

GET /api/messages/:id/replies Auth Fetches all replies to a message, ordered by creation time ascending. Hidden messages are excluded for non-admins.

When creating a message with a parentMessageId, the parent message's reply_count is incremented. Deleting a reply decrements it.

Commentable flag Top-level messages have an is_commentable field (default 1). When set to 0, no new replies can be added. Admins can toggle this via POST /api/messages/:id/moderate. This toggle is only available for top-level messages, not replies.

Message Operations

Sending Messages

POST /api/messages Auth Creates a new message in a channel or DM conversation.
ParamTypeDescription
contentstringMessage text (required unless imageUrls provided)
channelIdstringTarget channel (mutually exclusive with dmConversationId)
dmConversationIdstringTarget DM conversation
parentMessageIdstringOptional — makes this a thread reply
imageUrlsstringOptional comma-separated media URLs

On success, the message is broadcast to all relevant users via WebSocket (new_message event). The ChatBot user ID (57ba0e59-...) is excluded from membership checks.

Fetching Messages

GET /api/messages Auth Paginated message fetch. Returns up to 100 messages per page, newest first.
ParamTypeDescription
channelIdqueryFilter by channel
dmConversationIdqueryFilter by DM conversation
beforequeryCursor for pagination (ISO timestamp)

For private channels, messages created before the user's join date are excluded.

Editing Messages

PATCH /api/messages/:id Auth Edits a message. Only the author can edit their own messages. The original content is saved to message_edits for audit history. Broadcasts edit_message via WebSocket.

Deleting Messages

DELETE /api/messages/:id Auth Deletes a message. Authors can delete their own messages; admins can delete any message. Child thread replies are also deleted. Broadcasts delete_message via WebSocket.

Unread Tracking

POST /api/messages/read Auth Marks a channel or DM as read by updating last_read_at in the read_status table.
ParamTypeDescription
channelIdstringThe channel to mark read (or dmConversationId)
dmConversationIdstringThe DM to mark read (or channelId)
GET /api/unread Auth Returns the count of unread messages in a specific channel or DM. Excludes the user's own messages from the count.

The frontend polls unread counts and displays badges on channel/DM items in the sidebar. An audio alert plays on new incoming messages.


File Uploads

Files are uploaded via multipart form-data using multer, stored in Cloudflare R2 (when configured), and served from /uploads/.

Image Upload

POST /api/upload Auth Uploads an image. Field name: image. Max 5 MB. Allowed types: PNG, JPG, WebP.

Avatar Upload

POST /api/upload-avatar Auth Uploads a profile avatar. Field name: avatar. Max 5 MB. Allowed types: PNG, JPG, WebP.

Media Upload

POST /api/upload-media Auth Uploads images or video. Field name: file. Max 50 MB. Allowed types: PNG, JPG, WebP, MP4, WebM, MOV.
Storage path Files are stored in R2 under the key uploads/{uuid}.{ext} and served via the /uploads/:filename route which proxies from R2 with appropriate content-type headers.

Social Feed

The feed shows today's top-level messages from all channels and DMs the user has access to.

GET /api/feed Auth Returns up to 100 feed items for today, enriched with channel names, DM names, follow status, and seen status.

Ranking Algorithm

Feed items are sorted by four tiebreakers in order:

  1. Followed users first — messages from users the current user follows appear above others
  2. Unseen before seen — unread items rank higher
  3. Reply count — messages with more thread replies rank higher
  4. Recency — newer messages rank higher
POST /api/feed/seen Auth Marks feed items as seen. Accepts up to 100 message IDs.
ParamTypeDescription
messageIdsstring[]Array of message IDs to mark as seen
Private channel filtering Feed results exclude messages from private channels that were posted before the user joined that channel.

Follow System

POST /api/follow Auth Follow a user. Cannot follow yourself. Idempotent — following again is a no-op.
ParamTypeDescription
userIdstringThe user to follow
DELETE /api/follow/:userId Auth Unfollow a user.
GET /api/following Auth Returns an array of user IDs the current user follows.
GET /api/following/:userId Auth Checks if the current user follows a specific user. Returns { following: true/false }.

User Profiles

GET /api/users Auth Lists all users except the current user. Returns id, displayName, firstName, lastName, profileImageUrl.
POST /api/profile Auth Updates the current user's profile. Broadcasts profile_updated via WebSocket.
ParamTypeDescription
displayNamestringDisplay name
firstNamestringFirst name
lastNamestringLast name
profileImageUrlstringAvatar URL (from upload-avatar)
customRolestringCustom role label (e.g. "Designer")
taglinestringShort bio/tagline

Private Channels

Channels with isPrivate = 1 have restricted visibility:

Member Management

GET /api/channels/:id/members Auth Lists all members of a channel.
POST /api/channels/:id/members Admin Adds a user to a private channel. Broadcasts private_channel_added to the user.
DELETE /api/channels/:id/members/:userId Admin Removes a user from a channel.

Groups & Channel Restrictions

Groups are admin-defined user collections. When one or more groups are assigned to a channel, only members of those groups can access the channel. This is managed via the channel_groups junction table.

Group CRUD

GET /api/groups Admin Lists all groups with member counts.
POST /api/groups Admin Creates a new group.
PATCH /api/groups/:id Admin Renames a group.
DELETE /api/groups/:id Admin Deletes a group. Removes non-qualifying members from group-restricted channels. Broadcasts group_membership_changed.

Group Members

GET /api/groups/:id/members Admin Lists group members with display names and emails.
POST /api/groups/:id/members Admin Adds a user to a group. Auto-adds them to all channels restricted to that group.
DELETE /api/groups/:id/members/:userId Admin Removes a user from a group. If they have no other group granting access, they are also removed from the group's restricted channels.

Channel-Group Assignment

PUT /api/groups/:id/channels Admin Replaces the channel list for a group. Handles membership sync: adds members to new channels, removes from dropped channels (unless another group still grants access).
GET /api/groups/:id/channels Admin Lists channels assigned to a group.
POST /api/channels/:id/groups Admin Sets which groups restrict a channel (from the channel side). Syncs memberships accordingly.
Cascading effects Changing group-channel assignments can add or remove many users from channels at once. A group_membership_changed WebSocket event is broadcast to all affected users so their UIs refresh.

Roles & Permissions

Three roles exist, each with escalating privileges:

user Default role. Can send/edit/delete own messages, join public channels, manage DMs, update profile, invite others, follow/unfollow, use feed and search.
admin Everything a user can do, plus: create/edit/delete channels, manage private channel members, manage groups, view admin dashboard, moderate messages (hide/show), see hidden messages, export user data.
owner Everything an admin can do, plus: set user roles (promote/demote admins), activate/deactivate users, delete users entirely, force-logout users. There is exactly one owner.

WebSocket Protocol

The WebSocket server runs on the same HTTP server as Express. Clients connect via wss:// and authenticate through the sid session cookie parsed on upgrade.

Connection Lifecycle

Server-to-Client Events

new_message Broadcast when a message is sent. Includes the full message object with user details. Sent to all members of the target channel/DM.
edit_message Broadcast when a message is edited (content or link previews updated). Includes the updated message.
delete_message Broadcast when a message is deleted. Includes messageId, channelId/dmConversationId, and parentMessageId (if thread reply).
moderate_message Broadcast when a message is hidden or shown by an admin. Includes the updated message.
feed_update Broadcast to all connected users when any new top-level message is posted, signaling the feed should refresh.
force-refresh Broadcast to all users. Signals the client to reload all data (channels, messages, etc.). Triggered by admin actions like channel deletion or user deletion.
force-logout Sent to specific users when their account is deactivated. Client should destroy local session and redirect to login.
user_joined Broadcast to channel members when a new user joins. Includes user details.
profile_updated Broadcast to all users when someone updates their profile.
private_channel_added Sent to a specific user when they are added to a private channel.
group_membership_changed Sent to affected users when group memberships or group-channel assignments change.

Broadcast Functions

The server exposes two broadcast helpers:

ChatBot

The ChatBot is a synthetic user with a fixed ID (57ba0e59-cd68-459d-bb85-b3ec3db5cbd5). It is used for system-generated messages and is exempt from channel membership checks when posting.

The ChatBot user has role user and display name "ChatBot". It does not have an email or login credentials — it can only post messages server-side.


Admin API

User Management

GET /api/admin/users Admin Lists all users with roles, active status, and creation dates.
POST /api/admin/set-role Owner Sets a user's role to admin or user. Cannot change the owner's role or your own.
POST /api/admin/set-active Owner Activates or deactivates a user. Deactivation force-logs them out and deletes all their sessions.
POST /api/admin/delete-user/:userId Owner Permanently deletes a user and all their data (messages, memberships, invites, OTP codes). Thread reply counts are recalculated. Broadcasts force-refresh.

Force Refresh

POST /api/admin/refresh-all Admin Broadcasts a force-refresh event to all connected clients.

Message Moderation

POST /api/messages/:id/moderate Admin Toggles message visibility or commentability. Body: { field: "isVisible"|"isCommentable", value: 0|1 }. Visibility can be toggled on both top-level messages and replies. Commentability can only be toggled on top-level messages. Hidden messages are invisible to non-admin users in all views. Broadcasts moderate_message.

Dashboard Analytics

The admin dashboard provides real-time analytics via dedicated API endpoints. All dashboard endpoints require admin access.

GET /api/admin/dashboard/overview Returns aggregate counts: total users, active users, owners, admins, regular users, total messages, total channels, active sessions.
GET /api/admin/dashboard/activity 30-day activity breakdown by day. Separates channel messages, DM messages, and thread replies.
GET /api/admin/dashboard/top-channels Top 10 channels by message count in the last 30 days.
GET /api/admin/dashboard/top-users Top 10 users by message count in the last 30 days.
GET /api/admin/dashboard/user-rankings Per-day breakdown for the top 6 most active users over 30 days. Returns separate post and reply series for charting.
GET /api/admin/dashboard/content-breakdown Message content type distribution: text-only, with images, with video, with links.
GET /api/admin/dashboard/storage Database size in bytes, per-table sizes and row counts. DB limit is 1 GB.
GET /api/admin/dashboard/emoji-usage 30-day emoji reaction trends for the 6 supported emoji reactions.

Moderation

Admins and owners can moderate messages via the profile card popup. Click a message author's avatar to open the card, which displays moderation icons when in a channel context.

Commentable toggle The play/pause icon appears only on top-level messages (not replies). Click pause to lock the thread and prevent further replies; click play to unlock. Toggling is_commentable on a reply returns a 400 error.

Data Export

GET /api/admin/export-user-data/:userId Admin Exports all messages and edit history for a user as a CSV file download. Includes date, time, message type, context (channel name or DM), content, and media URLs.

CSV columns: Date, Time, Type, Context, Content, Media URLs. Edit history rows use type "Edit History" with the message ID as context.

Filename format export_{username}_{YYYY-MM-DD}.csv — special characters in the username are replaced with underscores.