--- 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.