Skip to content

fix(messaging): unblock local dev, let members create channels, name DMs - #96

Merged
Kavin-Charles merged 2 commits into
mainfrom
fix/messaging-module
Aug 3, 2026
Merged

fix(messaging): unblock local dev, let members create channels, name DMs#96
Kavin-Charles merged 2 commits into
mainfrom
fix/messaging-module

Conversation

@Kavin-Charles

Copy link
Copy Markdown
Collaborator

Context

Reported as "the messaging module has zero working features — private messages don't work and channel messages aren't visible."

The messaging module itself was largely fine. Most of the symptom was the local dev environment failing in a way that renders an empty page with no error. Two genuine bugs were found underneath, both confirmed against a running instance with a real non-admin user.

Environment fixes (5a207f6)

Three separate blockers made every module render an empty shell locally:

  • api published no host port. Compose mapped db (5432) and redis (6379) but left api as EXPOSE 3001 on the internal network only. Next's rewrite proxies /api/* to http://localhost:3001, so every API call died at the proxy. Now published on loopback.
  • @vencore/config pulled fs into the browser bundle. AppearanceControls.tsx is a client component importing the package barrel, which re-exports readConfigimport * as fs from 'fs'. Build failed with Module not found: Can't resolve 'fs'. Added a browser-safe @vencore/config/theme subpath export and repointed both importers.
  • Workspace packages must be built before next dev — runbook step, no code change. A stale pnpm store also needs pnpm install first.

Bug 1 — members could not create channels

POST /messaging/channels was gated on messaging:manage, which defaults to admin only. Every member hit 403 FORBIDDEN, so the sidebar's "New channel" button could never succeed.

Split channel creation into its own messaging:create_channel permission granted to admin and member. Rename/archive stay on the admin-only manage gate.

Adding a permission to a ModuleDefinition only reaches new workspaces — seedWorkspaceRoles runs once, when a workspace's Member role is created. Added a migration backfilling create_channel to every role already holding messaging:send. Roles with grants_all are unaffected; they short-circuit to superuser.

Bug 2 — every DM rendered as the literal string "dm"

DM channels are stored as name='dm', with the display name meant to be built from participants — but GET /messaging/channels returned no members, so the frontend had nothing to build from. Every DM row in the sidebar looked identical.

The list endpoint now attaches members for dm/group_dm rows only. A shared channelDisplayName() helper derives the label from the other participants, used by the sidebar row, channel header, and composer placeholder.

Verification

Seeded a second workspace user and drove the running app as a non-admin:

  • POST /channels201, channel appears in the sidebar
  • DM sidebar row, channel header, and placeholder all read "Admin" instead of "dm"
  • Channel history, sending, and realtime delivery over WebSocket all confirmed working

Tests: 13 new (router permission gates asserted off the router; channelDisplayName across regular channels, DMs, group DMs, and the not-yet-loaded and only-self fallbacks). Full suites green — 405 API, 43 web, 17 modules. Type-checks clean.

Base branch note

Targets main rather than development deliberately. origin/main already contains origin/development and is 50 commits ahead. AppearanceControls.tsx, packages/config/src/palette.ts, and presets.ts do not exist on development, and the fs bug only manifests because of them — so this branch does not apply there.

Known issue, not addressed here

The dev database's migration ledger is corrupted independently of this change: kysely_migration records 20260722/20260724/20260725 entries that don't exist in packages/db/migrations on either branch, so pnpm db:migrate refuses to run. The new migration was validated by applying its SQL directly; it is idempotent and will no-op once the ledger is reconciled.

Still open

Not fixed here: typing.start broadcasts workspace-wide instead of per-channel; GET /dms is dead code; no isError branch anywhere, so API failures render as empty UI with no feedback (this is what made the original report so hard to diagnose); the JWT is passed in the WebSocket query string where the cookie already works.

Three separate blockers made every module render an empty shell in local
development, which read as an application bug:

- The api service exposed 3001 but published no host port, so the Next.js
  rewrite proxied /api/* to a dead localhost:3001. Publish it on loopback.

- apps/web imports @vencore/config from a client component. The package
  barrel re-exports readConfig, which imports `fs`, so the browser bundle
  failed with "Module not found: Can't resolve 'fs'". Add a browser-safe
  ./theme subpath export and point both importers at it.

Workspace packages must also be built before `next dev`; that is a runbook
step rather than a code change.
Two defects confirmed against a running instance with a non-admin user.

Members could not create a channel. POST /messaging/channels was gated on
messaging:manage, which defaults to admin only, so every member got a 403
and the sidebar's "New channel" button could never succeed. Split channel
creation into its own messaging:create_channel permission granted to admin
and member, leaving rename/archive on the admin-only manage gate.

Adding a permission to a ModuleDefinition only affects new workspaces, since
seedWorkspaceRoles runs once when a workspace's Member role is created. Added
a migration backfilling create_channel to every role that already holds
messaging:send.

DM channels are all stored as name='dm' with the display name expected to be
built from participants, but GET /messaging/channels returned no members, so
every DM rendered as the literal string "dm". The list endpoint now attaches
members for dm/group_dm rows, and a shared channelDisplayName() helper derives
the label from the other participants for the sidebar, channel header, and
composer placeholder.

Covers both with tests: permission gates asserted off the router, and
channelDisplayName across regular channels, DMs, group DMs, and the
not-yet-loaded and only-self fallbacks.
Copilot AI review requested due to automatic review settings August 3, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restores local messaging usability and fixes two end-user messaging bugs by adjusting dev networking/bundling, refining messaging permissions, and improving DM naming in the UI/API.

Changes:

  • Publish the API container port for local Next.js rewrites and add a browser-safe @vencore/config/theme export to avoid bundling fs into client code.
  • Introduce messaging:create_channel permission (member+admin) and backfill it for existing workspaces via migration; keep messaging:manage admin-only.
  • Attach DM members in the channels list API and add a shared channelDisplayName() helper used across messaging UI, with tests.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/modules/src/messaging/index.ts Adds messaging:create_channel permission and documents intent.
packages/db/migrations/20260803_001_messaging_create_channel_permission.ts Backfills messaging:create_channel for roles that can already send messages.
packages/config/src/theme.ts Introduces browser-safe theming entrypoint for client imports.
packages/config/package.json Adds exports map including ./theme subpath.
docker-compose.yml Publishes API port on loopback for local dev web ↔ API connectivity.
apps/web/modules/shared/components/AppearanceControls.tsx Switches client import to @vencore/config/theme to avoid fs bundling.
apps/web/modules/messaging/lib/dm-name.ts Adds channelDisplayName() helper for DM/group DM naming.
apps/web/modules/messaging/lib/dm-name.test.ts Adds unit tests for channelDisplayName().
apps/web/modules/messaging/components/ChannelView.tsx Uses channelDisplayName() for DM header and composer placeholder.
apps/web/modules/messaging/components/ChannelSidebar.tsx Uses channelDisplayName() for DM rows; carries optional members.
apps/web/app/layout.tsx Switches to @vencore/config/theme import to keep server/client boundaries clean.
apps/api/src/routes/messaging/channels.ts Attaches members for DM/group DM rows in list endpoint; gates create on messaging:create_channel.
apps/api/src/tests/messaging-channels-permissions.test.ts Adds router-level permission gate assertions for messaging routes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +23 to +30
const isDm = channel.type === 'dm' || channel.type === 'group_dm';
if (!isDm) return channel.name;

const others = (channel.members ?? []).filter(m => m.user_id !== currentUserId);
if (others.length === 0) {
// Members not loaded yet, or a DM with only yourself left in it.
return channel.name === 'dm' ? 'Direct message' : channel.name;
}
@Kavin-Charles
Kavin-Charles merged commit cc91bef into main Aug 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants