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.
| Param | Type | Description |
| email | string | Email 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.
| Param | Type | Description |
| email | string | Email used in step 1 |
| code | string | 6-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.
| Param | Type | Description |
| email | string | Invitee's email (normalized to lowercase) |
| name | string | Optional 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.
| Param | Type | Description |
| name | string | Channel name (trimmed) |
| description | string | Optional description |
| isPrivate | number | 0 = 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.
| Param | Type | Description |
| userId | string | The 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.
| Param | Type | Description |
| content | string | Message text (required unless imageUrls provided) |
| channelId | string | Target channel (mutually exclusive with dmConversationId) |
| dmConversationId | string | Target DM conversation |
| parentMessageId | string | Optional — makes this a thread reply |
| imageUrls | string | Optional 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.
| Param | Type | Description |
| channelId | query | Filter by channel |
| dmConversationId | query | Filter by DM conversation |
| before | query | Cursor 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.
Message Search
GET /api/messages/search Auth
Searches message content using case-insensitive pattern matching (ILIKE).
| Param | Type | Description |
| query | string | Search text (minimum 2 characters) |
| scope | string | current (single channel/DM) or all (all accessible) |
| channelId | string | Required when scope=current and searching a channel |
| dmConversationId | string | Required when scope=current and searching a DM |
Results are limited to 50, ordered by relevance (exact match first, starts-with second, contains third) then by recency. Only top-level messages are searched (not thread replies).
Access enforcement
When scope=all, the search only includes channels/DMs the user is a member of. Private channel messages before the user's join date are excluded. Admins can see hidden (moderated) messages; regular users cannot.
Unread Tracking
POST /api/messages/read Auth
Marks a channel or DM as read by updating last_read_at in the read_status table.
| Param | Type | Description |
| channelId | string | The channel to mark read (or dmConversationId) |
| dmConversationId | string | The 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.
Link Previews
When a message contains URLs, the server fetches metadata (title, description, image, favicon) and stores it as JSON in the link_previews column. The frontend renders these as rich preview cards below the message text.
Fetch behavior
Link preview fetching happens asynchronously after the message is saved. The message is immediately broadcast, and when previews are ready, an edit_message event updates the message with the preview data.
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:
- Followed users first — messages from users the current user follows appear above others
- Unseen before seen — unread items rank higher
- Reply count — messages with more thread replies rank higher
- Recency — newer messages rank higher
POST /api/feed/seen Auth
Marks feed items as seen. Accepts up to 100 message IDs.
| Param | Type | Description |
| messageIds | string[] | 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.
| Param | Type | Description |
| userId | string | The 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.
| Param | Type | Description |
| displayName | string | Display name |
| firstName | string | First name |
| lastName | string | Last name |
| profileImageUrl | string | Avatar URL (from upload-avatar) |
| customRole | string | Custom role label (e.g. "Designer") |
| tagline | string | Short bio/tagline |
Private Channels
Channels with isPrivate = 1 have restricted visibility:
- Only members can see the channel in the channel list
- Members can only view messages posted after their join date
- Search results respect the join-date boundary
- Feed items from private channels before join date are excluded
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
- On connect: cookie is parsed, session is fetched from DB, user ID is extracted
- Unauthenticated connections are closed with code 4001
- Server sends a ping frame every 30 seconds; clients must respond with pong
- Clients that miss a pong cycle are terminated
- Clients can also send {"type":"ping"} JSON messages, which get {"type":"pong"} responses
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:
- broadcastToAll(data) — sends to every connected client
- broadcastToUsers(userIds, data) — sends only to clients matching the given user IDs
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.
- The eye icon toggles message visibility. Hidden messages (is_visible = 0) are invisible to regular users across all views. Admins see hidden messages faded at reduced opacity
- Visibility can be toggled on both top-level messages and replies
- Hiding a message does not delete it — click the eye icon again to restore it
- The moderation action broadcasts a moderate_message WebSocket event for real-time sync
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.