Context
In PR #1783 (user-identity foundation, PR-A of the team-foundation PRD at .claude/PRPs/prds/github-app-and-user-identity.prd.md) we plumbed user_id end-to-end. The implementation works correctly, but it surfaces an encapsulation inconsistency across the chat-platform adapters that's worth cleaning up before more team-features pile on top.
The inconsistency
Slack and Telegram adapters expose user identity via their onMessage callback arguments — the server-side handler receives normalized fields and never has to know the underlying platform's message shape:
// Slack — clean: the adapter pre-extracts userId + displayName
slackAdapter.onMessage(async event => {
const userId = await resolveUserId('slack', event.user, event.displayName);
…
});
// Telegram — same pattern: the callback exposes pre-resolved fields
telegramAdapter.onMessage(async ({ conversationId, message, userId: telegramUserId, displayName }) => {
const userId = await resolveUserId('telegram', telegramUserId, displayName);
…
});
Discord breaks the pattern — the server reaches into the raw discord.js Message object to pull author.id and author.username:
// Discord — the server has to know about message.author.{id,username}
discordAdapter.onMessage(async message => {
const userId = await resolveUserId('discord', message.author.id, message.author.username);
…
});
This means server/src/index.ts carries platform-specific knowledge for Discord that it doesn't carry for Slack or Telegram. The forge adapters (GitHub, Gitea, GitLab) should be audited for the same drift — webhook payloads have a sender.login (GitHub) / user.username (GitLab) shape that's adapter-specific.
Why it matters
- Inconsistent encapsulation — every new platform adapter has to re-decide where the user-extraction lives. The pattern should be one-direction: server consumes a normalized shape; adapter owns the conversion.
- Easier to add new forges — Gitea, GitLab, Bitbucket, etc. each have a different payload structure. If the server already does the extraction inline, adding a new forge means another
server/src/index.ts edit instead of a self-contained adapter.
- Testing surface — adapter-internal extraction is easier to unit-test in isolation than scattered server-handler code.
- Foundation for upcoming PRs — PR-B (GitHub App) and PR-C (per-user tokens,
requires: gate) both add more per-platform branching in the same hot path. Tidying now keeps those PRs focused.
Suggested cleanup
- Audit every platform adapter (
packages/adapters/src/chat/*, packages/adapters/src/forge/*, packages/adapters/src/community/*) for any user-identity extraction that currently happens server-side.
- Move each adapter's user-id + display-name extraction into the adapter itself.
- Expose normalized fields via the
onMessage callback signature — at minimum { platformUserId: string, displayName?: string } — so server/src/index.ts calls resolveUserId(adapter.getPlatformType(), platformUserId, displayName) uniformly.
- Consider hoisting the per-platform
resolveUserId calls into a shared adapter-bootstrap helper since they're now identical across platforms.
Out of scope
- The shared
resolveUserId(platform, platformUserId, displayName) helper itself — keep it where it is.
- DB schema — no changes.
- The actual user-creation transaction logic in
db/users.ts — already adapter-agnostic.
Effort
~1 day. Mechanical refactor; covered by existing tests + the per-adapter test suite. Pure DRY/encapsulation cleanup, no behaviour change.
When to ship
Recommended after PR-A merges (#1783) and before PR-B opens so the GitHub adapter's auth-strategy swap lands on the cleaner pattern. Worst case, can wait until between PR-B and PR-C.
Related
Context
In PR #1783 (user-identity foundation, PR-A of the team-foundation PRD at
.claude/PRPs/prds/github-app-and-user-identity.prd.md) we plumbeduser_idend-to-end. The implementation works correctly, but it surfaces an encapsulation inconsistency across the chat-platform adapters that's worth cleaning up before more team-features pile on top.The inconsistency
Slack and Telegram adapters expose user identity via their
onMessagecallback arguments — the server-side handler receives normalized fields and never has to know the underlying platform's message shape:Discord breaks the pattern — the server reaches into the raw discord.js
Messageobject to pullauthor.idandauthor.username:This means
server/src/index.tscarries platform-specific knowledge for Discord that it doesn't carry for Slack or Telegram. The forge adapters (GitHub, Gitea, GitLab) should be audited for the same drift — webhook payloads have asender.login(GitHub) /user.username(GitLab) shape that's adapter-specific.Why it matters
server/src/index.tsedit instead of a self-contained adapter.requires:gate) both add more per-platform branching in the same hot path. Tidying now keeps those PRs focused.Suggested cleanup
packages/adapters/src/chat/*,packages/adapters/src/forge/*,packages/adapters/src/community/*) for any user-identity extraction that currently happens server-side.onMessagecallback signature — at minimum{ platformUserId: string, displayName?: string }— soserver/src/index.tscallsresolveUserId(adapter.getPlatformType(), platformUserId, displayName)uniformly.resolveUserIdcalls into a shared adapter-bootstrap helper since they're now identical across platforms.Out of scope
resolveUserId(platform, platformUserId, displayName)helper itself — keep it where it is.db/users.ts— already adapter-agnostic.Effort
~1 day. Mechanical refactor; covered by existing tests + the per-adapter test suite. Pure DRY/encapsulation cleanup, no behaviour change.
When to ship
Recommended after PR-A merges (#1783) and before PR-B opens so the GitHub adapter's auth-strategy swap lands on the cleaner pattern. Worst case, can wait until between PR-B and PR-C.
Related
.claude/PRPs/prds/github-app-and-user-identity.prd.md