Submatter/.convoys/workspace-chat-foundation/brief-2-chat-realtime.md

56 lines
3.6 KiB
Markdown
Raw Permalink Normal View History

---
brief: 2
convoy: workspace-chat-foundation
role: role-implementer
title: Transactional outbox and realtime chat delivery
status: blocked
priority: P1
slice_dependencies:
- brief-1-chat-persistence
recommended_model: auto/coding
model_tier: standard
---
# Brief 2 — `Task-chat-realtime-delivery.md`
## Goal
Deliver committed chat events to authenticated subscribers across instances while preserving Postgres replay as the recovery path.
## Files
Expected scope; refine after brief 1 lands.
- `packages/database/src/schema/` — consume the outbox and per-channel sequence contracts from brief 1; add a generated migration only if claim/retry state genuinely requires a schema extension.
- `packages/shared/src/` — consume or refine the versioned chat event envelopes and `(channel_id, sequence)` cursor schemas from brief 1 without creating a second contract.
- `apps/collab-server/src/index.ts` and focused modules — authenticated WebSocket subscriptions and Redis fan-out.
- `apps/worker/src/` or the established background-job boundary — claim/publish/retry outbox records.
- `apps/web/` realtime client integration — authenticated connection and catch-up trigger only; visual chat UI belongs to brief 3.
- Adjacent tests for authorization, retries, ordering, reconnect, and Redis outage behavior.
## Required design
- Commands remain HTTP/tRPC; do not introduce browser gRPC.
- Brief 1 already commits message/read-state rows and outbox events atomically; this brief must preserve that boundary and must not publish before commit.
- Publishers are at-least-once; consumers and clients handle duplicates by outbox event ID and durable entity ID.
- Redis carries cross-instance fan-out, not canonical history.
- Subscriptions and replay use Brief 1's workspace AND context AND channel authorization, never workspace membership alone. Public is relative to the authorized context; private/direct also require membership. Archived channels allow authorized read/catch-up only. Recheck on delivery and permission changes, close revoked subscriptions and stop fan-out; a Redis topic or cached subscription is not authority. Object contexts remain rejected until the context-policy follow-on.
- Each subscribed channel tracks its own `(channel_id, sequence)` cursor. Reconnect catch-up queries Postgres independently for each authorized channel; no workspace-global ordering is implied.
- Message, thread-reply, tombstone, channel lifecycle, membership, and read-state events use one versioned envelope. `markChannelRead` invalidations update badges on the user's other devices.
- Reuse Hocuspocus/Yjs only for ephemeral presence, typing, or drafts.
## Acceptance
- A committed message remains queryable and is eventually delivered after Redis recovers.
- Two realtime instances fan the same event to their local authorized subscribers.
- Duplicate outbox publishes do not duplicate rendered durable messages or read-state changes.
- Multi-channel reconnect catches each channel up from its own sequence without assuming cross-channel ordering.
- Public, private, direct, and archived subscription rules match Brief 1 and unauthorized attempts disclose no data.
- A read-state event clears or recalculates unread state on another connected device.
- Heartbeat, bounded buffers/backpressure, graceful shutdown, and reconnect behavior are covered.
- `pnpm lint && pnpm type-check && pnpm test && pnpm build` pass.
## Prompt
Read `config/CursorSync.md` for collab-server lifecycle, the current Hocuspocus/Redis configuration, and brief 1's resulting schema/API. Keep durable messages out of Yjs. Document any deploy-impacting port or process changes explicitly.