Feature: Direct Messages & Private Channels (server-brokered, ACL-driven) #114

Open
opened 2026-07-11 05:37:07 +00:00 by icub3d · 0 comments
Owner

status:partial

Overview

Private 1:1 chats, private group chats, and (later) private group calls, modeled
as access-controlled channels on a shared server. There is no global DM
infrastructure and no cross-server federation: a private conversation lives on a
server all its participants are members of, and that server brokers it exactly
as it brokers public channels.

This is a deliberate pivot away from the E2EE-mailbox model in #16. This issue
covers the v1 transport/UX layer (server-brokered, trust-the-operator, plaintext
storage). End-to-end encryption of DM text and media is tracked separately as a
future hardening epic in #16 and layers on top of this work.

Full design: docs/design/direct-messages.md

Principles

  • Server-scoped, not global. A DM lives on a specific shared server; sharing
    three servers with someone means three separate DM contexts. Keeps identity
    portable (the Ed25519 pubkey) while keeping conversations local, and avoids
    reintroducing a central home-server anchor.
  • A private conversation is just a channel with an explicit member ACL. Text
    DMs, group chats, and group calls are the same primitive with different
    channel_type / feature bits — no new storage model, routing, or transport.
  • The server is a dumb, ACL'd pipe. No per-DM business logic; a lean operator
    can disable DMs via a feature flag and do less work.
  • Trust the operator now; E2E later (#16).

Punchlist

Phase 0 — Design sign-off

  • Agree on the channel-as-ACL model
  • Confirm channel_type names (dm, group_dm); decide if group DMs are v1 or deferred
  • Reconcile with the existing require_invite_for_dm config knob

Phase 1 — Data model & storage

  • Add channel_type values dm / group_dm (field exists, hardcoded to text today)
  • Add a channel_members ACL table: (channel_id, pubkey, added_at)
  • Storage-trait methods: create private channel, list a user's private channels, add/remove member, check membership
  • Canonicalize 1:1 DMs so a {A,B} pair maps to a single channel (idempotent open)

Phase 2 — Feature flag & config

  • Add direct_messages feature flag (default enabled) to FeatureFlags
  • Gate DM routes on require_feature(direct_messages)
  • Wire up require_invite_for_dm (already documented in server-model.md)
  • Optional: allow_group_dms, max_group_dm_members content-policy knobs

Phase 3 — REST API

  • POST /api/v1/dms — open or fetch the 1:1 DM with a target pubkey
  • GET /api/v1/dms — list the caller's private channels on this server
  • POST /api/v1/group-dms — create a group DM with N members
  • POST /api/v1/group-dms/{id}/members / DELETE .../{pubkey} — manage membership
  • Reuse existing message/attachment/reaction/thread endpoints, gated by ACL membership

Phase 4 — Gateway & visibility

  • Exclude private channels from the normal channel list; deliver only to ACL members
  • Route MESSAGE_CREATE (and friends) in a private channel only to connected sessions in the ACL
  • New events: DM_CHANNEL_CREATE, DM_CHANNEL_MEMBER_ADD / _REMOVE
  • ACL membership is the permission for a private channel

Phase 5 — Private voice / video / screen share

  • Ensure the SFU room is ACL-driven, not tied to a public voice channel (see #22–#24 and architecture.md)
  • A private group call = a dm / group_dm channel with voice enabled; room ACL = channel ACL
  • Feature-flag stacking: requires direct_messages and voice_channels (+ video / screen_share)
  • No new signaling — reuse the voice join/leave/state flow

Phase 6 — Client UX

  • Separate "Direct Messages" area, distinct from the server channel list
  • Start-DM entry points: profile, member list, mention (member list & sidebar done; profile/mention pending)
  • Group DM creation & member management UI
  • Private call UI reuses the voice/video/screen-share components

Out of scope (tracked elsewhere)

  • End-to-end encryption of DM text and media — #16 (future epic)
  • The SFU itself — #22 (voice), #23 (video), #24 (screen share); Phase 5 depends on these

Open questions

  • Canonical DM identity across leave/re-join: reuse old channel + history, or start fresh?
  • Group DM ownership: single owner, or any member can add/remove?
  • require_invite_for_dm semantics: "must be a server member" vs. "must have an explicit invite relationship"?
  • Notifications / unread state for the DM list — reuse per-channel read-state?
  • Blocking: can a user block a pubkey to refuse DMs, server-side or client-side?
status:partial ## Overview Private 1:1 chats, private group chats, and (later) private group calls, modeled as **access-controlled channels on a shared server**. There is no global DM infrastructure and no cross-server federation: a private conversation lives on a server all its participants are members of, and that server brokers it exactly as it brokers public channels. This is a deliberate pivot away from the E2EE-mailbox model in #16. This issue covers the **v1 transport/UX layer** (server-brokered, trust-the-operator, plaintext storage). End-to-end encryption of DM text and media is tracked separately as a future hardening epic in #16 and layers on top of this work. Full design: [`docs/design/direct-messages.md`](../src/branch/main/docs/design/direct-messages.md) ## Principles - **Server-scoped, not global.** A DM lives on a specific shared server; sharing three servers with someone means three separate DM contexts. Keeps identity portable (the Ed25519 pubkey) while keeping conversations local, and avoids reintroducing a central home-server anchor. - **A private conversation is just a channel with an explicit member ACL.** Text DMs, group chats, and group calls are the same primitive with different `channel_type` / feature bits — no new storage model, routing, or transport. - **The server is a dumb, ACL'd pipe.** No per-DM business logic; a lean operator can disable DMs via a feature flag and do less work. - **Trust the operator now; E2E later (#16).** ## Punchlist ### Phase 0 — Design sign-off - [ ] Agree on the channel-as-ACL model - [ ] Confirm `channel_type` names (`dm`, `group_dm`); decide if group DMs are v1 or deferred - [ ] Reconcile with the existing `require_invite_for_dm` config knob ### Phase 1 — Data model & storage - [x] Add `channel_type` values `dm` / `group_dm` (field exists, hardcoded to `text` today) - [x] Add a `channel_members` ACL table: `(channel_id, pubkey, added_at)` - [x] Storage-trait methods: create private channel, list a user's private channels, add/remove member, check membership - [x] Canonicalize 1:1 DMs so a `{A,B}` pair maps to a single channel (idempotent open) ### Phase 2 — Feature flag & config - [x] Add `direct_messages` feature flag (default enabled) to `FeatureFlags` - [x] Gate DM routes on `require_feature(direct_messages)` - [x] Wire up `require_invite_for_dm` (already documented in server-model.md) - [x] Optional: `allow_group_dms`, `max_group_dm_members` content-policy knobs ### Phase 3 — REST API - [x] `POST /api/v1/dms` — open or fetch the 1:1 DM with a target pubkey - [x] `GET /api/v1/dms` — list the caller's private channels on this server - [x] `POST /api/v1/group-dms` — create a group DM with N members - [x] `POST /api/v1/group-dms/{id}/members` / `DELETE .../{pubkey}` — manage membership - [x] Reuse existing message/attachment/reaction/thread endpoints, gated by ACL membership ### Phase 4 — Gateway & visibility - [x] Exclude private channels from the normal channel list; deliver only to ACL members - [x] Route `MESSAGE_CREATE` (and friends) in a private channel only to connected sessions in the ACL - [x] New events: `DM_CHANNEL_CREATE`, `DM_CHANNEL_MEMBER_ADD` / `_REMOVE` - [x] ACL membership *is* the permission for a private channel ### Phase 5 — Private voice / video / screen share - [ ] Ensure the SFU room is **ACL-driven**, not tied to a public voice channel (see #22–#24 and architecture.md) - [ ] A private group call = a `dm` / `group_dm` channel with voice enabled; room ACL = channel ACL - [ ] Feature-flag stacking: requires `direct_messages` **and** `voice_channels` (+ `video` / `screen_share`) - [ ] No new signaling — reuse the voice join/leave/state flow ### Phase 6 — Client UX - [x] Separate "Direct Messages" area, distinct from the server channel list - [ ] Start-DM entry points: profile, member list, mention (member list & sidebar done; profile/mention pending) - [x] Group DM creation & member management UI - [ ] Private call UI reuses the voice/video/screen-share components ## Out of scope (tracked elsewhere) - End-to-end encryption of DM text and media — **#16** (future epic) - The SFU itself — **#22** (voice), **#23** (video), **#24** (screen share); Phase 5 depends on these ## Open questions - Canonical DM identity across leave/re-join: reuse old channel + history, or start fresh? - Group DM ownership: single owner, or any member can add/remove? - `require_invite_for_dm` semantics: "must be a server member" vs. "must have an explicit invite relationship"? - Notifications / unread state for the DM list — reuse per-channel read-state? - Blocking: can a user block a pubkey to refuse DMs, server-side or client-side?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
icub3d/decentcom#114
No description provided.